자바(11) - 기본클래스
2021년08월06일Object 클래스
java.lang
우리가 지금까지 자바로 프로그래밍하면서 String, Integer와 같은 클래스를 사용했다. 위 클래스들은 우리가 불러오지도 않았는데 계속 쓰고있던게 이상하지 않은가. 이들은 java.lang 패키지에 속해 있다. String 클래스의 전체 이름은 java.lang.String이고, Integer 클래스의 전체 이름은 java.lang.Integer이다. 이처럼 java.lang 패키지에는 기본적으로 많이 사용하는 클래스들이 포함되어 있다. 근데 외부 패키지에 있는데 왜 import를 안할까. 이유는 컴파일할 때 java.lang 패키지를 import java.lang.*;
해서 자동으로 불러오기 때문이다.
최상위 클래스 Object
Object 클래스는 모든 자바 클래스의 최상위 클래스이다. 다시 말해 모든 클래스들은 Object 클래스로부터 상속을 받는다. 근데 얘또한 java.lang 패키지처럼 우리가 불러오지 않았다. 마찬가지로 컴파일러가 변환하면서 extends Object를 자동으로 써주기 때문이다. Object를 상속받으면 다음과 같은 메서드를 사용할 수 있다.
- String toString(): 객체를 문자열로 표현하여 반환한다. 재정의하여 객체에 대한 설명이나 특정 멤버 변수 값을 반환한다.
- boolean equals(Object object): 두 인스턴스가 동일한지 여부를 반환한다. 재정의하여 논리적으로 동일한 인스턴스임을 정의할 수 있다.
- int hashCode(): 객체의 해시 코드 값을 반환
- Object clone(): 객체를 복제하여 동일한 멤버 변수 값을 가진 새로운 인스턴스 생성
- Class getClass(): 객체의 Class 클래스를 반환
- void finalize(): 인스턴스가 힙 메모리에서 제거될 때 가비지 컬렉터(GC)에 의해 호출되는 메서드이다. 네트워크 연결 해제, 열려 있는 파일 스트림 해제 등을 구현한다.
- void wait(): 멀티스레드 프로그램에서 사용하는 메서드이다. 스레드를 ‘기다리는 상태(non runnalbe)’로 만든다.
- void notify(): wait() 메서드에 의해 기다리고 있는 스레드(non runnalbe)상태를 실행 가능한(runnable)상태로 가져온다.
toString() 메서드
해당 메서드는 객체 정보를 문자열로 바꾸어준다. String이나 Integer처럼 미리 재정의가 된 것이 있다. 기본적으로 객체를 프린트해보면 클래스 이름@해시 코드 값
형태로 보일 것이다. 기본적으로 getClass().getName() + '@' + Integer.toHexString(hashCode())
가 실행되기 때문이다.
equals() 메서드
equlas 메서드는 두 인스턴스의 주소 값을 비교하여 boolean 값을 반환해준다. 주소 값이 같으면 당연히 같은 인스턴스이난. 서로 다른 주소값이어도 같은 인스턴스라고 정의하는 경우도 있다. 즉 물리적 동일성뿐 아니라 논리적 동일성을 구현할 때 사용된다.
// 인자는 stduentID, 학번이라 생각하자
Student student = new Student(100);
Student student2 = student
Student student3 = new Student(100);
위의 코드를 보면 인스턴스를 만들어서 student에 참조값을 저장하였고, student2도 같은 주소값을 가리키도록 지정했다. 이런 경우 두 개의 물리적 주소가 같기에 같은 인스턴스인 것이다. 그런데 student3같은 경우는 새로 new 해서 만든 인스턴스이기에 물리적 주소가 다르다. 그런데 학번은 같으니 같은 인스턴스라고 생각해야 한다. 이럴 때 equals를 활용해서 같은 학생으로 처리해주는 것이다.
class Student {
@Override
public boolean equals(Object obj) {
if(obj instanceof Student) {
Student std = (Student)obj;
if(this.studentID == std.studentID) {
return true;
} else {
return false;
}
}
return false;
}
public static void test() {
System.out.print(student == student3) // 이것은 주소값을 비교하는 거기에 false
System.out.print(student.equals(student3)) // 위의 오버라이드로 인해 true 반환
}
}
hashCode() 메서드
해시(hash)는 정보를 저장하거나 검색할 때 사용하는 자료 구조이다. 정보를 어디에 저장할 것인지, 어디서 가져올 것인지 해시 함수를 사용하여 구현한다. 해시 함수는 객체의 특정 정보(키 값)를 매개변수 값으로 넣으면 그 객체가 저장되어야 할 위치나 저장된 해시 테이블 주소(위치)를 반환하다. 따라서 객체 정보를 알면 해당 객체의 위치를 빠르게 검색할 수 있다. hashCode()메서드는 자바 가상 머신이 힙 메모리에 저장한 인스턴스의 주소 값을 알려준다. 인스턴스가 같다면 hashCode()의 값이 같아야 한다. 그렇기에 논리적으로 같은 인스턴스의 해쉬값도 동일하게 나오게 재정의 해주어야 한다.
class Studnent {
@Override
public int hashCode() {
return studentId;
}
}
위처럼 hashCode를 갖고오면 학번을 리턴하게 만들었다. 그러면 논리적으로 같은 인스턴스는 같은 해쉬값을 반환할 것이다.
clone() 메서드
객체 원본을 유지해 놓고 복사본을 사용한다거나, 기본 틀(prototype)의 복사본을 사용해 동일한 인스턴스를 만들어 복잡한 생성 과정을 간단히 하려는 경우에 clone() 메서드를 사용하면 된다.
class Point {
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
public String toString() {
return "x = " + x + "," + "y= " + y;
}
}
// Clonable 인터페이스를 함께 선언하여 객체를 복제해도 된다 지정한다.
class Circle implements Clonable {
Point point;
int radius;
Circle(int x, int y, int radius) {
this.radius = radius;
point = new Point(x, y);
}
public String toString() {
return "원점은 " + point + "이고," + "반지름은 " + radius + "이다.";
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
String 클래스
String을 선언하는 방법은 두 가지가 있다.
- 생성자의 매개변수로 문자열 생성
String str1 = new String("abc");
- 문자열 상수를 가리키는 방식
String str2 = "test";
이 둘의 차이로는 new 예약어를 사용하면 힙 메모리에 객체를 만드는 것이고, 2번째 방법은 문자열 상수를 가리키는 것인데, 상수플에 있는 “test”라는 문자열 상수의 메모리 주소를 가리키게 된다. 그래서 위 두개를 ==
연산자를 사용해서 비교해보면 다른 주소에 위치하기에 false가 나오고 eqauls메서드로 했을 시에 true가 나온다. String 문자열은 public final char value[]
라는 곳에 저장이 된다. final로 선언되어있기에 한 번 생성된 문자열은 변경되지 않는다. 이런 문자열의 특징을 immutable(불변)하다고 한다. 만약 두 개의 문자열을 연결한다면 어떻게 될까. 문자열이 변경되는게 아니라 새로운 문자열이 생성된다.
String javaStr = new String("java");
String androidStr = new String("android");
System.out.println(System.identityHashCode(javaStr));
javaStr = javaStr.concat(androidStr);
System.out.println(System.identityHashCode(javaStr));
아래처럼 concat의 사용해보고 해쉬값을 봐보면 달라지는 것을 볼 수 있다. 이런식으로 합치게 된다면 분명 메모리 낭비가 생김은 자명할 것이다. 이런 문제를 해결하기 위해 사용하는 클래스로는 StringBuffer
와 StringBuilder
클래스가 있다. 이 둘은 final로 지정하지 않은 char[]변수를 활요하여 사용한다. 두 클래스의 차이로는 여러 작업(스레드)이 동시에 문자열을 변경하려 할 때 문자열이 안전하게 변경되도록 보장해 주는가 그렇지 않은가의 차이이다. StringBuffer는 문자열이 안전하게 변경되도록 보장하지만 StringBuilder는 보장되지 않는다. 멀티 스레드 환경이 아니면 StringBuilder가 좀 더 빠르다.
public class StringBuilderTest {
public static void main(String[] args) {
String javaStr = new String("java");
StringBuilder buffer = new StringBuklder(javaStr);
System.out.println(System.identityHashCode(buffer));
buffer.append(" and");
buffer.append(" android");
buffer.append(" programming is fun");
System.out.println(System.identityHashCode(buffer));
javaStr = buffer.toString();
System.out.println(System.identityHashCode(javaStr));
}
}
buffer의 주소값은 변경이 되지 않음을 확인할 수 있을 것이다. 반면 buffer.toString()한 부분은 또 새로이 메모리가 할당됨을 볼 수 있다.
Wrapper 클래스
우리는 정수를 사용할 때 primitive(기본)형인 int를 사용했다. 근데 정수를 객체형으로 사용해야 하는 경우가 있다.
public void setValue(Integer i) {} // 객체를 매개변수로 받는 경우
public Integer returnValue() {} // 반환 값이 객체형인 경우
자바는 기본 자료형처럼 사용할 수 있는 클래스들을 제공한다. 이러한 클래스를 기본 자료형을 감쌌다는 의미로 Wrapper 클래스라고 한다.
- Boolean: boolean
- Byte: byte
- Character: char
- Short: short
- Integer: int
- Long: long
- Float: float
- Double: double
Interger 클래스
Interger 클래스의 경우 int 자료형을 감싼 클래스인데, 다음과 같은 매개변수를 받을 수 있다.
Integer(int value) {} // 특정 정수를 매개변수로 받기
Integer(String s) {} // 특정 문자열을 매개변수로 받기
Integer는 클래스다 보니 여러 기능을 제공하는데 int형의 최댓값, 최솟값을 상수로 정의하고 여러 메서드들도 있다.
Integer iValue = new Integer(100);
int myValue = iValue.intValue(); // int값 가져오기
// valueOf 정적 메서드를 사용하면 생성자 없이 바로 Integer 클래스로 반환받는다.
Integer number1 = Integer.valueOf("100"); // 숫하형 문자열도 가능하다.
Integer number2 = Integer.valueOf(100);
int num = Integer.pareInt("100"); // 문자열이 어떤 숫자를 나타낼 때 int로 변환 가능
오토박싱, 언박싱
정수를 int로 선언하는 것과 Integer로 선언하는 경우는 전혀 다르다. int는 기본 4바이트 자료형이지만, Integer는 클래스이기에 생성하려면 생성자를 호출하고 정수 값을 인수로 넣어야 한다. 이처럼 기본 자료형과 Wrapper 클래스는 같은 값을 나타내지만, 쓰임과 특성이 전혀 다르다. 그래서 자바 5이전에는 이 둘을 연산하려면 하나의 형으로 일치시켜 줘야 했다. 자바 5부터는 다음처럼 해도 연산이 잘 된다.
Integer num1 = new Integer(100);
int num2 = 200;
int sum = num1 + num2; // num.intValue()로 변환(언박싱)
Integer num3 = num2; // Integer.valueOf(num2)로 변환(오토박싱)
기본형을 객체형으로 바꾸는 것은 오토박싱(autoboxing), 객체형을 기본형으로 꺼내는 것을 언박싱(unboxing)이라고 한다. 컴파일러가 알아서 잘 변경해주니 아주 좋다.
Class 클래스
자바의 모든 클래스와 인터페이스는 컴파일이 되고 나면 class 파일로 생성된다. 예를 들어 a.java 파일이 컴파일되면 a.class 파일이 생성된다. 이 class 파일에는 클래스나 인터페이스에 대한 변수, 메서드, 생성자 등의 정보가 들어있다. Class 클래스는 컴파일 된 class 파일에 저장된 클래스나 인터페이스 정보를 가져오는데 사용한다.
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");
public class Person {
private String name;
private int age;
public Person() {}
public Person(String name) {
this.name = name;
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age)
}
public class ClassTest {
public void main(String[] args) throws ClassNotFoundException {
Person person = new Person();
Class pClass1 = person.getClass();
System.out.println(pClass1.getName());
Class pClass2 = Person.class;
System.out.println(pClass2.getName());
Class pClass3 = Class.forName("classex.Person") // 패키지 풀 네임 써줘야한다.
}
}
classex.Person classex.Person classex.Person
위에는 클래스 가져오는 3가지 방식으로 해본 것이다. 세 번째 같은 경우에 문자열로 입력받았는데 만약에 해당 문자열에 해당하는 클래스가 존재하지 않으면 ClassNotFoundException이 발생한다.
Class 클래스를 활용해 클래스 정보 알아보기
프로그래밍을 하다 보면 내가 사용할 클래스의 자료형을 모르는 경우가 있을 수 있다. 예로 들어 내 컴퓨터에 저장되어 있지 않은 객체를 메모리에 로드하고 생성하는 경우 그 객체의 정보를 알 수 없다. 이때 Class 클래스를 가져올 수 있다면 해당 클래스 정보, 즉 생성자, 메서드, 멤버 변수 정보를 찾을 수 있다. 이렇게 사용하려는 클래스의 자료형을 모르는 상태에서 Class 클래스를 활용하여 그 클래스의 정보를 가져오고, 이 정보를 활용하여 인스턴스를 생성하거나 메서드를 호출하는 방식을 리플렉션(reflection)
이라고 한다. Class 클래스의 몇 가지 메서드를 활용해보자. Constructor, Method, Field등을 활용해볼건데 이것들은 java.lang.reflect 패키지에 정의되어 있다.
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class StringClassTest {
public static void main(String[] args) {
Class strClass = Class.forName("java.lang.String");
// 모든 생성자 가져오기
Constructor[] cons = strClass.getConstructor();
for(Constructor c: cons) {
System.out.println(c);
}
// 모든 멤버 변수(필드) 가져오기
Field[] fields = strClass.getFields();
for(Field f : fields) {
System.out.println(f);
}
// 모든 메서드 가져오기
Method[] methods = strClass.getMethods();
for(Method m : methods) {
System.out.println(m)
}
}
}
newInstance()로 클래스 생성하기
Class 클래스 메서드 중에 newInstance() 메서드를 활용하면 인스턴스를 만들 수 있다. 이 메서드는 Object를 반혼하므로 생성된 객체형으로 형 변환 해주어야 한다.
public class NewInstanceTest {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
Person person1 = new Person();
Class pClass = Class.forName("Person");
Person person2 = (Person)pClass.newInstance();
}
}
Class 클래스를 사용하는 방법은 클래스의 자료형을 직접 사용하여 프로그래밍하는 것보다 더 복잡하고, 예외 처리도 해야한다. 이미 알고 있는 클래스인 경우와 컴파일할 때 직접 참조할 수 있는 클래스는 Class 클래스를 활용할 필요가 없다. 이런 리플렉션 프로그래밍은 컴파일 시점에 알 수 없는 클래스, 즉 프로그램 실행 중에 클래스를 메모리에 로딩하거나 객체가 다른 곳에 위치해서 원격으로 로딩하고 생성할 때 사용하는 것이 좋다.
Class.forName()으로 동적 로딩하기
대부분의 클래스 정보는 프로그램이 로딩될 때 이미 메모리에 있다. 그런데 이런 경우가 있다. 어떤 회사에서 개발한 시스템이 있는데, 그 시스템은 여러 종류의 데이터 베이스를 지원한다. 오라클, MySQL, MS-SQL 등이 있다. 그렇다고 이 시스템을 컴파일할 때 모든 데이터베이스 라이브러리를 같이 컴파일할 필요가 없다. 어떤 데이터베이스와 연결할지만 결정하면 해당 드라이버만 로딩하면 된다. 회사가 사용하는 데이터베이스 정보는 환경 파일에서 읽어 올 수도 있고 다른 변수 값으로 받을 수도 있다. 즉 프로그램 실행 이후 클래스의 로딩이 필요한 경우 클래스의 동적 로딩(dynamic loading
방식을 사용한다. 이를 Class.forName() 메서드로 동적 로딩할 수 있다.
Class pClass = Class.forName("classex.Person");
String className = "classex.Person";
Class pClass = Class.forName(className);
위의 두 방식으로 로딩할 수가 있다. 대신 유의할 점이 있다. 클래스 이름이 문자열 값이므로, 문자열에 오류가 있어도 컴파일할 때에는 그 오류를 알 수 없다. 결국 프로그램이 실행되고 메서드가 호출될 때 클래스 이름에 해당하는 클래스가 없다면 ClassNotFoundException이 발생한다. 즉 동적 로딩 방식은 컴파일할 때 오류를 알 수 없다. 하지만 여러 클래스 중 하나를 선택한다거나, 시스템 연동 중 매개변수로 넘어온 값에 해당하는 클래스가 로딩되고 실행되는 경우에는 동적 로딩 방식을 유연하게 사용할 수 있다.