기본적으로 사용해오던 'String', 'Integer', 'System' 등등 기본 클래스들이 속한 패키지는
프로그래밍 할 때에 import 하지 않아도 사용할 수 있었습니다.
pre-compile 단계에서 자동으로 'import java.lang.*;' 문장이 추가되기 때문입니다.
java.lang 패키지 아래의 Object
는 자바의 모든 클래스의 최상위 클래스입니다.
컴파일러가 'extends Object'를 추가해 모든 클래스는 이 클래스를 상속받습니다.
그리고 Object 클래스의 메서드를 사용할 수 있고 일부는 재정의 할 수 있습니다.
(final로 선언된 메서드는 재정의 할 수 없습니다.)
Object 클래스 파일을 보면 아래와 같은 메서드들이 있습니다.(자바 버전에 따라 상이합니다.)
public final native Class<?> getClass();
public native int hashCode();
public boolean equals(Object obj);
public String toString();
public final native void notify();
public final native void notifyAll();
public final void wait();
public final native void wait(long timeoutMillis);
public final void wait(long timeoutMillis, int nanos);
화면 우클릭 - Source - Override/Implement Methods... (또는 단축키 ⌥⌘S) 를 누르면
아래와 같이 재정의가 가능한 메서드를 보여줍니다.
객체의 정보를 'String'으로 바꿔 사용할 수 있습니다.
'String', 'Integer'은 'toString()' 메서드가 재정의 되어있어 각각 문자열, 정수값을 반환합니다.
class Book {
String title;
String author;
Book(String title, String author) {
this.title = title;
this.author = author;
}
// toString() 메서드 재정의
@Override
public String toString() {
return title + ", " + author;
}
}
public class ToStringEx {
public static void main(String[] args) {
Book book = new Book("두잇자바", "박은종");
System.out.println(book); // 오버라이딩 전 object.Book@cb0ed20, 오버라이딩 후 두잇자바, 박은종
// String 클래스에 toString이 재정의되어있어 문자열 자체를 출력하도록 합니다.
String str = new String("test");
System.out.println(str); // test
}
}
두 인스턴스의 주소 값을 비교하여 'true' 또는 'false'를 반환합니다.
재정의 하여 두 인스턴스가 논리적으로 동일한지 여부를 반환합니다.
'String', 'Integer'은 'equals()' 메서드가 재정의 되어있어 각각 문자열, 정수값을 비교합니다.
class Student {
int studentID;
String studentName;
Student (int studentID, String studentName) {
this.studentID = studentID;
this.studentName = studentName;
}
// equals() 메서드 재정의
@Override
public boolean equals(Object obj) {
if (obj instanceof Student) { // obj 타입이 Student 이면
Student std = (Student)obj; // 변수 std에 obj를 Student로 다운캐스팅 하여 저장
if (studentID == std.studentID) { // 인스턴스의 studentID와 매개변수의 studentID를 비교
return true;
} else {
return false;
}
}
return false;
}
}
public class EqualsTest {
public static void main(String[] args) {
String str1 = new String("test");
String str2 = new String("test");
System.out.println(str1 == str2); // 물리적인 상태 비교, 같은 주소인지. false
System.out.println(str1.equals(str2)); // 논리적인 상태 비교, 문자열이 같은지. true
// equals() 메서드 재정의 후
Student std1 = new Student(10001, "Tomas");
Student std2 = new Student(10001, "Tomas");
System.out.println(std1 == std2);
System.out.println(std1.equals(std2));
}
}
'hash'는 정보를 저장, 검색하기 위해 사용하는 자료구조입니다.
힙 메모리에 인스턴스가 저장되는 방식이기도 합니다.
자료의 특정 key 값에 대해 저장 위치를 반환해주는 해시 함수를 사용하는데
'hashCode()' 메서드는 인스턴스의 저장 주소(힙 메모리 주소)를 10진수로 반환합니다.
키 값만 알면 데이터가 저장된 위치를 알 수 있어 편리합니다.
서로 다른 메모리의 두 인스턴스 주소가 같다면
재정의 된 equals() 메서드의 값이 true, 동일한 hashCode() 반환값을 가져야 합니다.
논리적인 동일함을 위해 equals() 메서드를 재정의 했다면 일반적으로
hashCode() 메서드 또한 재정의 하여 동일한 값이 반환되도록 합니다.
'String' 클래스에서는 동일한 문자열 인스턴스에 대해 동일한 정수가 반환 되고
'Integer' 클래스에서는 동일한 정수값의 인스턴스에 대해 같은 정수값이 반환 됩니다.
// 위 코드에 이어서
class Student {
...
@Override
public int hashCode() {
// 위의 equals 에서 사용한 멤버변수가 서로 같으면 동일한 해시코드값을 반환하면 됩니다.
return studentID;
}
}
public class EqualsTest {
public static void main(String[] args) {
String str1 = new String("test");
String str2 = new String("test");
Student std1 = new Student(10001, "Tomas");
Student std2 = new Student(10001, "Tomas");
...
// 해시코드
// hashCode() 메서드가 재정의 되어있기 때문에 str1, 2의 주소값은 같습니다.
// equals() 메서드를 재정의해서 논리적으로 같은 두 개의 인스턴스는 동일한 해시코드 값을 반환합니다.
System.out.println("str1, str2의 hashCode 비교");
System.out.println(str1.hashCode());
System.out.println(str2.hashCode());
System.out.println("\nstr1, str2의 실제 힙 메모리 hashCode 비교(인스턴스의 해시코드)");
System.out.println(System.identityHashCode(str1));
System.out.println(System.identityHashCode(str2));
System.out.println("\nstd1, std2의 hashCode 비교");
System.out.println(std1.hashCode());
System.out.println(std2.hashCode());
System.out.println("\nstd1, std2의 실제 힙 메모리 hashCode 비교");
System.out.println(System.identityHashCode(std1));
System.out.println(System.identityHashCode(std2));
}
}
객체의 원본을 유지한 채 복제할 때 사용합니다.(깊은 복사)
기본 틀(prototype)을 두고 복잡한 생성 과정을 반복하지 않고 복제 할 수 있습니다.
'clone()'메서드를 사용하면 객체의 정보(멤버변수 값)가 같은 인스턴스가 또 생성되는 것이므로
객체 지향 프로그램의 정보은닉, 객체 보호의 관점에 위배 될 수 있습니다.
객체의 'clone()'메서드 사용을 허용한다는 의미로 아래와 같이 Cloneable
인터페이스를 명시합니다.
class Circle implements Cloneable {
}
// 클래스 Point
class Point {
int x;
int y;
// 생성자
Point(int x, int y) {
this.x = x;
this.y = y;
}
// 인스턴스를 sysout으로 출력하면 나타나는 반환값
public String toString() {
return "x=" + x + ", " + "y=" + y;
}
}
// 클래스 Circle, 'Cloneable' 명시
class Circle implements Cloneable{
Point point;
private int radius;
// 생성자
public Circle(int x, int y, int radius) {
point = new Point(x, y);
this.radius = radius;
}
// 인스턴스를 sysout 으로 출력하면 반환되는 결과값
public String toString() {
return "원점은 " + this.point + "이고 반지름은 " + radius + " 입니다.";
}
// clone() 메서드 오버라이딩
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class ObjectCloneTest {
// Cloneable 할 수 없는데 사용하는 경우 예외 처리.
public static void main(String[] args) throws CloneNotSupportedException {
Circle circle = new Circle(10, 20, 5);
Circle cloneCircle = (Circle)circle.clone();
// 다른 결과 출력됨
System.out.println(System.identityHashCode(circle));
System.out.println(System.identityHashCode(cloneCircle));
// 같은 결과 출력됨![](https://velog.velcdn.com/images/k1m2njun/post/899f8fc6-3849-48f2-a23b-5491bbf9c294/image.png)
System.out.println(circle);
System.out.println(cloneCircle);
}
}
[1] 힙 메모리에 인스턴스로 생성하는 방법
[2] 상수 풀(Constant Pool)에 있는 주소를 참조하는 방법
String str1 = new String("abc"); // [1] 생성자의 매개변수로 문자열 생성
String str2 = "abc"; // [2] 문자열 상수를 가리키는 방식
[1], [2]는 메모리 영역에 대한 차이를 갖습니다.
아래 코드의 결과를 확인하면 인스턴스를 생성해 만든 문자열은 인스턴스마다 각기 다른 주소값을 가지지만
같은 문자열을 변수에 저장하면 해당 문자열이 가진 주소값이 공유됩니다.
public class StringTest {
public static void main(String[] args) {
// [1]
String str1 = new String("abc");
String str2 = new String("abc");
System.out.println(str1 == str2);
// [2]
String str3 = "abc";
String str4 = "abc";
System.out.println(str3 == str4);
}
}
한 번 생성된 String 값(문자열)은 변하지 않습니다.(immutable)
그래서 두 개의 문자열을 연결하면 새로운 인스턴스가 생성됩니다.
public class StringTest {
public static void main(String[] args) {
String str1 = new String("java");
String str2 = new String("programing");
System.out.println(System.identityHashCode(str1)); // 1693847660
str1 = str1.concat(str2);
System.out.println(str1);
// 두 문자열을 연결했더니 메모리 주소가 바뀌었습니다.
System.out.println(System.identityHashCode(str1)); // 2050019814
}
}
이렇게 문자열 연결을 계속하면 메모리에 garbage가 많이 생길 수 있습니다.
내부적으로 가변적인 char[] 배열을 가지고 있는 클래스입니다.
매번 새로 생성하지 않고 기존 배열을 변경하므로 위처럼 garbage가 생기지 않기 때문에
문자열을 여러번 연결하거나 변경 할 때 사용하면 유용합니다.
StringBuffer
는 멀티 쓰레드 프로그래밍에서 동기화(synchronization)를 보장합니다.
단일 쓰레드 프로그램에서는 StringBuilder
를 사용하는 것을 권장합니다.
사용하다가 'String' 클래스로 반환하고 싶다면 'toString()' 메서드를 사용할 수 있습니다.
public class StringBuilerTest {
public static void main(String[] args) {
String str1 = new String("java");
System.out.println("=== 'st1'의 메모리 주소");
System.out.println(System.identityHashCode(str1));
StringBuilder buffer = new StringBuilder(str1);
System.out.println("=== 기본 'buffer'의 메모리 주소");
System.out.println(System.identityHashCode(buffer));
buffer.append(" and");
buffer.append(" android");
System.out.println("=== 문자열을 추가한 'buffer'의 메모리 주소");
System.out.println(System.identityHashCode(buffer));
System.out.println("=== 결과");
System.out.println(buffer);
}
}
=== 'st1'의 메모리 주소
1429880200
=== 기본 'buffer'의 메모리 주소
2050019814
=== 문자열을 추가한 'buffer'의 메모리 주소
2050019814
=== 결과
java and android
기본형 | Wrapper 클래스 |
---|---|
boolean | Boolean |
byte | Byte |
char | Character |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
'Integer'는 객체이고, 'int'는 4Byte 기본 자료형입니다.
두 데이터를 같이 연산할 때 자동으로 변환이 일어납니다.
Integer num1 = new Integer(100);
int num2 = 200;
int sum = num1 + num2; // 언박싱 : 'num1.intValue()'로 변환
Integer num3 = num2; // 오토박싱 : 'Integer.valueOf(num2)'로 변환
// int 자료형의 데이터를 Integer 객체의 데이터로 변환
현재는 컴파일이 되지만 9 버전 이후로 이런 사용방식이 사용되지 않으며 추후 제거될 수 있습니다.
The constructor Integer(int) has been deprecated since version 9 and marked for removal
자바의 모든 클래스와 인터페이스는 컴파일 후 .class 파일로 생성됩니다.
해당 파일에는 객체의 정보(멤버 변수, 메서드, 생성자 등)가 포함되어 있고
'Class' 클래스는 컴파일된 class 파일에서 객체의 정보를 가져올 수 있습니다.
로컬에서는 모르는 클래스나 메서드를 가져다 쓸 때, 클래스의 정보를 알아와서 진행할 수 있습니다.
이런 프로그램을 리플렉션 프로그램이라고 합니다.
[1] Object 클래스의 getClass() 메서드
String s = new String(); Class c = s.getClass(); // getClass() 메서드의 반환형은 Class
[2] 클래스 파일 이름을 Class 변수에 직접 대입
Class c = String.Class;
[3] Class.forName("클래스명") 메서드 사용
Class c = Class.forName("java.lang.String");
[3] 은 위 두 가지와는 다르게 클래스 이름을 String으로 클래스 메모리에 올리는,
동적로딩 역할을 하는 메서드입니다.
package classex;
public class Person {
String name;
int age;
public Person() {}
public Person(String name) {
this.name = name;
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Getter, Setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class ClassTest {
public static void main(String[] args) throws ClassNotFoundException {
Person person = new Person();
Class pClass1 = person.getClass();
// 이미 인스턴스가 생성되어 있으면 getClass를 사용, Object의 메서드
// Class 클래스를 반환합니다.
System.out.println(pClass1.getName()); // 패키지명.클래스명
Class pClass2 = Person.class;
System.out.println(pClass2.getName()); // 패키지명.클래스명
// 위 두 방법은 Person 클래스가 컴파일 되어서 존재해야 사용할 수 있음. 정적로딩 static loading
// 아래 방법은 문자열을 읽어 해당 클래스를 로드해서 찾습니다. 동적로딩
👉 Class pClass3 = Class.forName("classex.Person");
// 여기서 패키지명.클래스명 을 문자열로 입력하기 때문에 처음에는 확실하지 않아 컴파일되지 않으므로
// throws 를 통해 예외처리를 합니다.
// 만약 틀린 클래스명을 입력하면 콘솔에 에러가 표시됩니다.
System.out.println(pClass3.getName());
}
}
Reflection Programing
: Class 클래스를 이용해 클래스의 정보를 가져오고
이를 활용해 인스턴스를 생성, 메서드를 호출하는 등의 프로그래밍 방식입니다.
로컬 메모리에 객체가 없어서 객체의 자료형을 직접 알 수 없는 경우(원격에 객체가 있는 경우 등)
객체 정보만을 이용해 프로그래밍 할 수 있습니다.
Constructor, Method, Filed 등 'java.lang.reflect' 패키지에 있는 클래스들을 활용합니다.
일반적으로 자료형을 알 수 있는 경우엔 사용하지 않습니다.
동적 로딩
이란 컴파일 시에 데이터 타입이 모두 바인딩 되어 자료형이 로딩되는 것이 아니라
실행 중 데이터 타입을 찾아 바인딩 되는 방식입니다. 전자를 정적 로딩(static loading)
이라고 합니다.
프로그래밍 할 때 어떤 클래스를 사용할지 모르면 이를 먼저 변수로 처리하고,
실행이 될 때 해당 변수에 대입된 값(클래스)이 실행될 수 있도록
'Class' 클래스에서 제공하는 static 메서드입니다. 이렇듯 실행 시에 로딩 되므로
경우에 따라 다른 클래스가 사용될 수 있어서 유용합니다.
컴파일 타임에 체크 할 수 없으므로 해당 문자열에 대한 클래스가 없는 경우에
예외(ClassNotFoundException)이 발생할 수 있습니다.
이에 대한 예시는 바로 위 코드블럭에서 확인할 수 있습니다.(👉 표시)
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class StringTest {
public static void main(String[] args) throws ClassNotFoundException {
Class strClass = Class.forName("java.lang.String");
Constructor[] cons = strClass.getConstructors();
// String 생성자를 모두 출력합니다.
for (Constructor c: cons) {
System.out.println(c);
}
Field[] fields = strClass.getFields();
// String 필드를 모두 출력합니다.
for (Field f : fields) {
System.out.println(f);
}
Method[] methods = strClass.getMethods();
// String 메서드와 상속받은 메서드를 모두 보여줍니다.
for (Method m : methods) {
System.out.println(m);
}
}
}
public class ClassTest {
public static void main(String[] args) throws InstantiationException {
Person person = new Person();
Class pClass = Class.forName("classex.Person");
Person p2 = (Person)pClass.newInstance();
// Type mismatch: cannot convert from Object to Person
// 리턴 타입은 Object여야 하기 때문에 컴파일 되지 않습니다.
// 예외 처리까지 해야 컴파일이 가능합니다.
// newInstance() 메서드를 사용하면 디폴트 생성자가 호출됩니다.
System.out.println(person);
System.out.println(p2);
}
}