[Java] java 자바의 클래스들
목차
01. Object 클래스 - 모든 클래스의 최상위 클래스
java.lang 패키지
- 프로그래밍 시 import 하지 않아도 자동으로 import 된다.
import java.lang.*;- 많이 사용하는 기본 클래스들이 속한 패키지다.
String,Integer,System등이 여기 포함된다.
모든 클래스는 Object 클래스를 상속받는다
java.lang.Object클래스는 모든 클래스의 최상위 클래스다.- 모든 클래스는 Object에서 상속받으며, Object 클래스의 메서드 중 일부는 재정의해서 사용할 수 있다.
- 컴파일러가
extends Object를 자동으로 추가한다.
class Student => class Student extends Object
toString() 메서드
- 객체의 정보를 String으로 바꾸어서 사용할 때 쓰인다.
String이나Integer클래스는 이미 재정의되어 있다.
toString() 메서드 재정의 예:
class Book{
private String title;
private String author;
public Book(String title, String author) {
this.title = title;
this.author = author;
}
public String toString() {
return title + "," + author;
}
}
public class BookTest {
public static void main(String[] args) {
Book book = new Book("데미안", "헤르만 헤세");
System.out.println(book);
}
}

위 코드에서 toString()을 주석 처리하고 실행하면 다음과 같이 나온다. 앞은 클래스 풀네임(패키지 이름까지), 뒤는 인스턴스의 메모리 위치(가상 메모리 값)다. JVM 값이며 실제 물리 메모리 값은 아니다. 16진수로 표시된다.

반면 String 값을 하나 만들어서 출력해보면 문자열이 제대로 나온다. 그 이유는 다음과 같다.

String 클래스 안에 toString이 정의되어 있어서 바로 호출되기 때문이다. 그래서 우리도 타이틀과 저자를 바로 출력하고 싶으면 toString()을 오버라이딩해서 바꾸면 된다.

이처럼 객체에 대한 정보를 출력할 때 toString() 메서드를 많이 재정의해서 사용하게 된다.
02. Object 클래스의 메서드 활용
인스턴스의 물리적 동일성 vs 논리적 동일성
인스턴스를 힙 메모리에 만들면 학생에 대한 메모리가 힙에 잡힌다. 학생을 또 만들면 얘도 새로 잡힌다. 두 인스턴스가 같다는 것은 힙 메모리 위치가 같다는 뜻이며, 변수의 주소값을 찍었을 때 똑같으면 같은 것이다. 이때 ==로 동일 여부를 판단한다.
그런데 물리적으로는 다르지만 논리적으로 같을 경우가 있다. 한 학생에 대해 여러 인스턴스가 생길 수 있지만 같은 학생으로 봐야 하는 경우다. 예를 들어 학번이 같거나 사원번호가 같으면 같은 사람으로 본다.
equals()는 원형이 주소값을 비교한다. 그러나 재정의해서 논리적으로 true/false를 반환하게 할 수 있다.

만약 학번이 100번인 학생이 두 명이면 물리적으로는 다르지만 논리적으로는 같은 학생으로 본다. ==는 주소값만 비교하지만, equals를 오버라이딩하면 논리적 비교가 가능하다.
해시(hash)와 hashCode의 관계
주소값을 해시코드라 한다. 해시가 들어가 있는 이유는 자바가 힙 메모리를 관리하는 방식이 해시 방식이기 때문이다.
- 키값이 있고, 그 키값에 대해 오브젝트를 어디에 저장할지 지정하는 방식이다.
- 키값을 알면 인덱스를 지정할 수 있다. 키값을 해시 함수에 넣어 인덱스를 저장하고, 그 키값을 찾아가는 것이 해시 함수다. 키는 유일하다(중복 불가).
- 자바에서 JVM이 메모리를 관리하는 방식이 해시 방식이며, 그 안의 값을 해시값이라 한다.
논리적으로 같다고 하면 그 해시코드 값이 같아야 한다. 단, 물리적으로도 같은지는 별개이며, 물리적인 해시코드 값을 꺼내보는 함수는 따로 있다. 그래서 equals와 hashCode는 페어(pair)로 볼 수 있다.
String도 equals가 오버라이딩되어 있어 같은 문자열이면 true를 반환하는데, 그때 해시코드를 반환해보면 같다. String 클래스는 내부적으로 hashCode와 equals가 오버라이드되어 있고, 문자열을 조합해 숫자값(integer)으로 나타낸 해시값을 반환한다.
equals() 메서드
- 두 인스턴스의 주소 값을 비교하여 true/false를 반환한다.
- 재정의하여 두 인스턴스가 논리적으로 동일한지 여부를 구현한다.
- 인스턴스가 다르더라도 논리적으로 동일한 경우 true를 반환하도록 재정의할 수 있다. (같은 학번, 같은 사번, 같은 아이디의 회원 등)
hashCode() 메서드
hashCode()는 인스턴스의 저장 주소를 반환한다.- 힙 메모리에 인스턴스가 저장되는 방식이 hash 방식이다.
- hash: 정보를 저장하고 검색하는 자료구조다.
- 자료의 특정 값(키 값)에 대한 저장 위치를 반환해주는 해시 함수를 사용한다.

두 인스턴스가 같다는 것은 다음을 의미한다:
- 두 인스턴스에 대한
equals()의 반환 값이 true이고 - 동일한
hashCode()값을 반환한다.
따라서 논리적 동일함을 위해 equals() 메서드를 재정의했다면, hashCode() 메서드도 재정의하여 동일한 hashCode 값이 반환되도록 한다.
Student.java
public class Student {
private int studentId;
private String studentName;
public Student(int studentId, String studentName)
{
this.studentId = studentId;
this.studentName = studentName;
}
public boolean equals(Object obj) {
<!-- 넘어온 obj가 student인지 확인 아니면 비교할 필요도 없어서 false.
맞으면 student로 바꿔준다. -->
if( obj instanceof Student) {
Student std = (Student)obj;
if(this.studentId == std.studentId )
//
return true;
else return false;
}
return false;
}
@Override
public int hashCode() {
return studentId;
}
}

위에서 hashCode와 equals를 오버라이딩해서 재정의했다.
if( obj instanceof Student) {
넘어온 obj가 Student인지 확인한다. 아니면 비교할 필요도 없으므로 false. 맞으면 Student로 형변환해준다.
EqualTest.java
package ch02;
public class EqualTest {
public static void main(String[] args) throws CloneNotSupportedException {
Student Lee = new Student(100, "Lee");
Student Lee2 = Lee;
Student Shun = new Student(100, "Lee");
System.out.println(Lee == Shun);
System.out.println(Lee.equals(Shun));
System.out.println(Lee.hashCode());
System.out.println(Shun.hashCode());
Integer i1 = new Integer(100);
Integer i2 = new Integer(100);
System.out.println(i1.equals(i2));
System.out.println(i1.hashCode());
System.out.println(i2.hashCode());
System.out.println(System.identityHashCode(i1));
System.out.println(System.identityHashCode(i2));
Student Lee3 = (Student)Lee.clone();
System.out.println(System.identityHashCode(Lee));
System.out.println(System.identityHashCode(Lee3));
Lee.setStudentName("kim");
Student copyStudent = (Student)Lee.clone();
//생성을 하는게 아니라 인스턴스 그대로 복제해서 만드는 함수
//하위클래스도 그대로 복제할 떄 사용 가능.
System.out.println(copyStudent);
}
}

==는 다르게(false) 나오고,equals도 다르게 나온다. 일반hashCode는 메모리가 다르기 때문에 다르게 나온다.- 단, 재정의한
hashCode를 찍으면 같은 값(100, 100)이 나와야 한다.
진짜 해시코드 값은 어떻게 알 수 있을까? System.identityHashCode를 쓰면 진짜 해시코드 값을 출력해준다(static 함수).
clone() 메서드
객체를 new 했을 때는 생성자가 호출되고 일련의 과정을 거쳐 객체가 생성된다. 반면 clone을 쓰면 멤버 변수의 상태(status)가 똑같은 객체를 생성한다. 즉, 오브젝트의 상태를 그대로 복제한다. 하나의 프로토타입을 붕어빵처럼 찍어내는 셈이다.
생성자와 다른 점:
- 생성자는 초기화를 해서 처음 값을 가지고 생성한다.
- clone은 기존 객체를 그대로 복제한다.
clone은 private 멤버까지 복사하므로 객체지향에서 정보 은닉이나 보호 관점에서 위배될 수 있다. 그래서 명시적으로 선언해서 구현한다.
implements Cloneable
반드시 이 부분을 선언해서 사용해야 한다(복제 가능하다고 선언한 것과 마찬가지다).

물론 clone도 재정의가 가능하다.
- 객체의 원본을 복제하는 데 사용하는 메서드다.
- 생성 과정의 복잡한 과정을 반복하지 않고 복제할 수 있다.
clone()메서드를 사용하면 객체의 정보(멤버 변수 값 등)가 동일한 또 다른 인스턴스가 생성되므로, 객체지향 프로그래밍의 정보 은닉, 객체 보호 관점에서 위배될 수 있다.- 해당 클래스의
clone()메서드 사용을 허용한다는 의미로Cloneable인터페이스를 명시해 준다.
Student.java
public class Student implements Cloneable{
.......
@Override
protected Object clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
return super.clone();
}
}
EqualTest.java
Student Lee3 = (Student)Lee.clone();
System.out.println(System.identityHashCode(Lee));
System.out.println(System.identityHashCode(Lee3));
clone의 리턴 타입은 Object인데 위에서 Student로 형변환했다.

Cloneable을 선언하지 않고 그냥 짜면 에러가 난다. 위에서 말했듯 public class Student implements Cloneable을 선언하고 실행하면 정상 동작한다.
03. String, StringBuilder, StringBuffer 클래스, text block
String 클래스
- 자바 13부터 스트링 블럭(text block)을 지원하며, HTML/JSON 문자열을 만드는 데 유용하다.
- 스트링을 만드는 방법은
new로 생성하거나, 리터럴을 assign하는 두 가지가 있다.
String 선언하기:
String str1 = new String("abc");
String str2 = "abc";
- 힙 메모리에 인스턴스로 생성되는 경우와, 상수 풀(constant pool)에 있는 주소를 참조하는 두 가지 방법이 있다.
- 힙 메모리는 생성될 때마다 다른 주소 값을 가지지만, 상수 풀의 문자열은 모두 같은 주소 값을 가진다.
public class StringTest {
public static void main(String[] args) {
String str1 = new String("abc");
String str2 = new String("abc");
System.out.println(str1 == str2);
String str3 = "abc";
String str4 = "abc";
System.out.println(str3 == str4);
}
}
위 코드를 실행해보면 new로 생성한 str1 == str2는 false, 리터럴 str3 == str4는 true가 나온다.
- 한 번 생성된 String은 불변(immutable)이다.
- String을 연결하면 기존 String에 연결되는 것이 아니라 새로운 문자열이 생성된다(메모리 낭비가 발생할 수도 있다).

public class StringTest2 {
public static void main(String[] args) {
String java = new String("java");
String android = new String("android");
System.out.println(System.identityHashCode(java));
java = java.concat(android);
System.out.println(java);
System.out.println(System.identityHashCode(java));
}
}
연결 전후의 해시코드를 비교하면 두 어드레스가 다르다. 즉, 연결한 스트링은 다른 어드레스가 된다(java 따로, android 따로 생성).
StringBuilder, StringBuffer 활용하기
- String은 final(불변)이지만, StringBuilder/StringBuffer는 가변적이라 바뀔 수 있다.
- 내부의 char[] 버퍼를 변경하므로 유용하게 사용할 수 있다.
- 마지막에
toString()으로 String을 얻는다. - StringBuffer는 멀티스레드에서 동기화를 제공한다.
- 단일 스레드에서는 StringBuilder를 사용한다.
- 멀티스레드에서는 StringBuffer를 사용한다.
정리하면:
- 내부적으로 가변적인 char[]를 멤버 변수로 가진다.
- 문자열을 여러 번 연결하거나 변경할 때 사용하면 유용하다.
- 새로운 인스턴스를 생성하지 않고 char[]를 변경한다.
- StringBuffer는 멀티 쓰레드 프로그래밍에서 동기화(synchronization)를 보장한다.
- 단일 쓰레드 프로그램에서는 StringBuilder 사용을 권장한다.
toString()메서드로 String을 반환한다.
public class StringBuilderTest {
public static void main(String[] args) {
String java = new String("java");
String android = new String("android");
StringBuilder buffer = new StringBuilder(java);
System.out.println(System.identityHashCode(buffer));
buffer.append("android");
System.out.println(System.identityHashCode(buffer));
java = buffer.toString();
}
}
해시코드를 찍어보면 append 전후가 동일하다. 아무리 연결해도 버퍼는 가변하므로 같은 객체가 변환된다. String 인자로 쓸 일이 많을 때 toString을 사용한다.

text block 사용하기 (java 13부터 지원)
- 문자열을
""""""사이에 이어서 만들 수 있다. - HTML, JSON 문자열을 만드는 데 유용하게 사용할 수 있다(백틱 방식과 유사).
JSON은 키값으로 이루어지고, 태그나 뉴라인을 넣으면 지저분하고 복잡한데, 백틱처럼 선언해서 바로 사용할 수 있다(자바 8에서는 에러가 난다).

public class StringTextBlock {
public static void main(String[] args) {
String strBlock = """
This
is
text
block
test.""";
System.out.println(strBlock);
System.out.println(getBlockOfHtml());
}
public static String getBlockOfHtml() {
return """
<html>
<body>
<span>example text</span>
</body>
</html>""";
}
}
04. Class 클래스 사용하기
Class 클래스
클래스 이름 자체가 Class인 클래스다. 컴파일을 하면 자바의 클래스나 인터페이스마다 .class 파일이 생기고 그 안에 정보들이 다 들어 있는데, 그 정보를 가져오거나 인스턴스를 생성하는 것이 Class 클래스다.
- 자바의 모든 클래스와 인터페이스는 컴파일 후 class 파일이 생성된다.
- Class 클래스는 컴파일된 class 파일을 로드하여 객체를 동적 로드하고, 정보를 가져오는 메서드를 제공한다.
Class.forName("클래스 이름")메서드로 클래스를 동적으로 로드한다.

레퍼런스를 보면 클래스들이 많은데, 꼭 기억해야 할 게 forName이다.
forName의 따옴표 안에 클래스 네임이 들어가는데, 해당 스트링 네임에 대응되는 클래스나 인터페이스에 대해 매핑(동적 바인딩)을 해준다.
Class c = Class.forName("java.lang.String");
위는 String 클래스에 대한 풀네임을 써서 자바 String 클래스를 가져온다. 가져와서 동적 로딩을 해 런타임(실행 시)에 필요한 클래스를 로딩시켜 사용한다(다른 언어에도 동적 로딩 방식이 있다).
클래스 이름으로 직접 Class 클래스 가져오기:
Class c = String.class;
생성된 인스턴스에서 Class 클래스 가져오기:
String s = new String();
Class c = s.getClass(); //Object 메서드
동적 로딩
- 컴파일 시에 데이터 타입이 binding 되는 것이 아니라, 실행(runtime) 중에 데이터 타입을 binding 하는 방법이다.
- 프로그래밍 시에는 문자열 변수로 처리했다가 런타임 시에 원하는 클래스를 로딩하여 binding 할 수 있다는 장점이 있다.
- 컴파일 시에 타입이 정해지지 않으므로, 동적 로딩 시 오류가 발생하면 프로그램에 심각한 장애가 발생할 수 있다.
컴파일 시에는 무엇을 로드할지 모르지만 런타임 시 로드하는 경우가 있다. 예를 들어 JDBC 드라이버는 어떤 데이터를 읽기 전에 라이브러리가 무엇이 될지 모른다. 그럴 때 변수를 갖고 있다가 오라클이면 오라클 드라이버를 넣어준다. 그래서 위 코드처럼 변수만 써주고 런타임 시 각각에 맞게 로딩한다.
스트링 값이라 어떤 값이든 들어갈 수 있다는 장점이 있다.
단점은 컴파일 시 그 데이터가 (로컬에) 없거나 스펠링이 틀려서 클래스를 찾지 못하면 에러가 나거나 셧다운될 수 있다는 점이다. 시스템이 돌아가다가 부르는 것이라 그렇다.
Class의 newInstance() 메서드로 인스턴스 생성
new키워드를 사용하지 않고 클래스 정보를 활용하여 인스턴스를 생성할 수 있다.
객체를 생성할 때 보통 new로 생성자를 호출했는데, new가 아니라 newInstance() 메서드를 사용해서 생성한다.
클래스 정보 알아보기 (Reflection)
- reflection 프로그래밍: Class 클래스를 사용하여 클래스의 정보(생성자, 변수, 메서드)를 알 수 있고, 인스턴스를 생성하고 메서드를 호출하는 방식의 프로그래밍이다.
- 로컬 메모리에 객체가 없는 경우, 원격 프로그래밍, 객체의 타입을 알 수 없는 경우에 사용한다.
java.lang.reflect패키지에 있는 클래스를 활용하여 프로그래밍한다.- 일반적으로 자료형을 알고 있는 경우에는 사용하지 않는다.
StringTest.java
public class StringTest {
public static void main(String[] args) throws ClassNotFoundException {
Class c3 = Class.forName("java.lang.String");
Constructor<String>[] cons = c3.getConstructors();
for(Constructor con: cons) {
System.out.println(con);
}
System.out.println();
Method[] methods = c3.getMethods();
for(Method method : methods) {
System.out.println(method);
}
}
}
위는 동적 로딩으로 가져와서 String 정보를 보여주는 것이다. Class c3 = Class.forName("java.lang.String");에서 클래스를 찾지 못하면 ClassNotFoundException이 떨어진다.
Person.java
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) {
this.age = age;
}
public String toString() {
return name;
}
}
ClassTest.java
package ch04;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class ClassTest {
public static void main(String[] args) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException,
ClassNotFoundException, NoSuchMethodException, SecurityException {
Person person = new Person("James");
System.out.println(person);
Class c1 = Class.forName("ch04.Person");
Person person1 = (Person)c1.newInstance();
System.out.println(person1);
Class[] parameterTypes = {String.class};
Constructor cons = c1.getConstructor(parameterTypes);
Object[] initargs = {"김유신"};
Person personLee = (Person)cons.newInstance(initargs);
System.out.println(personLee);
//이 위에서 한게 이 한줄과 동일
Person kim2 = new Person("kim");
}
}
getConstructor로 어떤 생성자를 찾을지 파라미터 타입을 명시해야 한다. 그 위에 Class 배열로 파라미터 타입을 선언한다.
Class[] parameterTypes = {String.class};
그러면 해당하는 생성자를 찾아 반환해준다.
Constructor cons = c1.getConstructor(parameterTypes);
argument 타입은 array다.
이 일련의 작업이 아래 한 줄과 동일하다:
Person kim2 = new Person("kim");
이런 코딩은 로컬에 Person 타입이 없는데 그 객체를 생성하고 생성자 정보를 가져와서 사용해야 할 때 쓴다. 여기서 로컬은 같은 프로세스(메모리)를 의미하며, 서로 다른 프로세스는 전혀 다르다. 같은 메모리 안에서도 이렇게 사용할 수 있다.