Welcome! Everything is fine.

[4-1] 인터페이스 활용하기(3) ~ 기본 클래스(2) 본문

자격증 및 기타 활동/J2KB

[4-1] 인터페이스 활용하기(3) ~ 기본 클래스(2)

개발곰발 2021. 9. 1.
728x90

💡 인터페이스 요소

기존에는 상수추상메서드가 인터페이스의 구성요소였지만, 자바8 부터 다른 요소들이 추가되었다. 추가된 메서드는 다음과 같다.

메서드 설명
디폴트 메서드 default 키워드를 사용하며, 기본 구현을 가지는 메서드. 구현 클래스에서 재정의할 수 있다. 만약 재정의 하지 않으면 디폴트 메서드의 기존 내용이 들어간다. 하지만 재정의 할 수 있어도 인터페이스를 인스턴스화 할 수는 없다.
정적(static) 메서드 인스턴스 생성과 상관없이 인터페이스 타입으로 사용할 수 있는 메서드. 어떤 클래스가 그 인터페이스를 구현하지 않아도 인터페이스에 선언된 메서드를 new하지 않고 호출해서 쓸  때 사용한다.
private 메서드 인터페이스 내부에서만 기능을 제공하기 위해 구현하는 메서드. 인터페이스를 구현한 클래스에서 사용하거나 재정의할 수 없다. 

디폴트 메서드 예시

기존에는 추상 메서드만 사용하면 모든 클래스에서 구현코드가 똑같은 메서드일지라도 하나하나 각자 구현을 해야했다. 이를 보완하기 위해 디폴트 메서드를 제공하기 시작했다. 따라서 인터페이스에 구현 코드가 있는 메서드를 넣고자 할 때아래와 같이 default 키워드를 사용하여 디폴트 메서드를 만들어야 한다. default를 지우면 인터페이스에서는 구현코드를 가질 수 없으므로 오류가 난다. 물론 이렇게 디폴트 메서드를 구현했더라도, 다른 클래스에서 재정의 할 수 있다.

public interface Calc {

    double PI = 3.14;
    int ERROR = -999999999;
	
    int add(int num1, int num2);
    int substract(int num1, int num2);
    int times(int num1, int num2);
    int divide(int num1, int num2);
	
    default void Description() { // default를 지우면 에러
        System.out.println("정수 계산기를 구현합니다.");
    }
public class CalculatorTest {

    public static void main(String[] args) {

        int num1 = 10;
        int num2 = 2;
        
        Calc calc = new CompleteCalc();
        System.out.println(calc.add(num1, num2));
        
        calc.Description();
    }
}

위와 같이 호출한 결과는 다음과 같다.

12
정수 계산기를 구현합니다.

정적(static) 메서드 예시

정적 메서드 역시 디폴트 메서드와 마찬가지로 구현코드가 있는 메서드이다. static 키워드를 사용하여 적는다. 디폴트 메서드와 정적 메서드 둘 다 구현코드를 제공하고 있는데, 이 둘의 차이점은 무엇일까? 디폴트 메서드는 기본적인 구현코드가 그냥 제공되는 것이고, 정적 메소드는 인스턴스 생성과 상관없이 인터페이스의 이름만으로 호출해서 사용할 수 있는 메서드이다.

public interface Calc {

    double PI = 3.14;
    int ERROR = -999999999;
	
    int add(int num1, int num2);
    int substract(int num1, int num2);
    int times(int num1, int num2);
    int divide(int num1, int num2);
	
    static int total(int[] arr) {
        int total = 0;
		
        for(int i: arr) {
            total += i;
        }
        return total;
    }
}

아래와 같이 인스턴스를 생성하지 않고 인터페이스의 이름만으로 호출하여 사용할 수 있다.

public class CalculatorTest {

    public static void main(String[] args) {

        int[] arr = {1, 2, 3, 4, 5};
        int sum = Calc.total(arr); 
    }
}

 

실행 결과는 다음과 같다.

15

private 메서드 예시

private 키워드를 사용하며, 하위 클래스에서 재정의하거나 사용할 수 없다. private static 이 붙은 메서드는 static 메서드에서 사용하고, 그냥 private 메서드는 디폴트 메서드에서 사용한다.

public interface Calc {

    double PI = 3.14;
    int ERROR = -999999999;
	
    int add(int num1, int num2);
    int substract(int num1, int num2);
    int times(int num1, int num2);
    int divide(int num1, int num2);
	
    default void Description() {
        System.out.println("정수 계산기를 구현합니다.");
        MyMethod();
    }
    
    static int total(int[] arr) {
        int total = 0;
		
        for(int i: arr) {
            total += i;
        }
        myStaticMethod();
        return total;
    }
    
    private void MyMethod() {
        System.out.println("private 메서드 입니다.");
    }
	
    private static void myStaticMethod() {
        System.out.println("private static 메서드 입니다.");
    }
}

두 개의 인터페이스 구현하기

public interface Buy {

    void buy();
	
    default void order() {
        System.out.println("구매 주문");
    }
}
public interface Sell {

    void sell();
	
    default void order() {
        System.out.println("판매 주문");
    }
}

Buy 인터페이스의 buyer라는 변수에 대입을 하면 buy() 메서드만 사용할 수 있고, Sell 인터페이스의 seller라는 변수에 대입을 하면 sell() 메서드만 사용할 수 있다. 물론 Customer 타입일때는 둘 다 사용할 수 있다. 어떤 변수에 대입했는지에 따라 호출할 수 있는 메서드가 제한된다. 또한, Buy 인터페이스와 Sell 인터페이스에서 디폴트 메서드가 중복되는데, 이때 그 디폴트 메서드를 오버라이딩 할  수 있다.

public class Customer implements Buy, Sell{

    @Override
    public void sell() {
        System.out.println("판매하기");
		
    }

    @Override
    public void buy() {
        System.out.println("구매하기");
		
    }

    @Override
    public void order() {
        System.out.println("고객 판매 주문");
    }
}
public class CustomerTest {

    public static void main(String[] args) {
        Customer customer = new Customer();
		
        Buy buyer = customer;
        buyer.buy();
		
        Sell seller = customer; 
        // Sell 이라는 인터페이스에 대입을 했기 떄문에 Sell이 제공하는 메서드만 호출가능
        seller.sell();
		
        // 어떤 변수에 대입했냐에 따라 호출할 수 있는 메서드가 한정적.

        customer.order();
        buyer.order();
        seller.order();
    }
}

출력 결과는 다음과 같다.

구매하기
판매하기
고객 판매 주문
고객 판매 주문
고객 판매 주문

인터페이스 상속

인터페이스 간에도 상속이 가능하다. 그러나  구현코드의 상속이 아니라 타입만 상속되는 형 상속(type inheritance)이다. 인터페이스는 여러 개 상속 가능이 가능하다.

public interface X {

   void x();
}
public interface Y {

    void y();
}

클래스인 경우에는 하나만 상속할 수 있지만,  인터페이스는 다중상속이 가능하다.

public interface MyInterface extends X, Y{

    void myMrthod();
}

MyInterface 인터페이스에서 X, Y 인터페이스를 상속하고 있으므로 기본적으로 아래 클래스에서는  3개의 메서드를 상속해야한다. 그러나 어떤 타입에 대입되느냐에 따라 사용할 수 있는 메서드는 한정적일 수 있다. 

public class MyClass implements MyInterface{

    @Override
    public void x() {
        System.out.println("x()");
		
    }

    @Override
    public void y() {
        System.out.println("y()");
		
    }

    @Override
    public void myMrthod() {
        System.out.println("myMethod()");
		
    }
	
    public static void main(String[] args) {
		
        MyClass myClass = new MyClass();
		
        X xClass = myClass;
        xClass.x();
        
        Y yClass = myClass;
        yClass.y();
		
        MyInterface myMethod = myClass;
        myMethod.myMethod();
    }
}

출력 결과는 다음과 같다.

x()
y()
myMethod()

인터페이스 구현과 클래스 상속 함께 사용하기

인터페이스 구현과 클래스 상속을 함께 사용할 수도 있다.  extends 클래스 implements 인터페이스 의 형식으로 많이 사용한다. 여기서 인터페이스는 여러 개 올 수 있다. 또한 실제 프레임워크(스프링, 안드로이드)를 사용하면 클래스르 상속받고 여러 인터페이스를 구현하는 경우가 종종 있다고 한다.

💡 기본 클래스

기본클래스란 자바에서 제공하는 기본적인 클래스를 말한다.

java.lang 패키지

  • 많이 사용하는 기본 클래스들(String, Integer, System 등)이 속한 패키지이다.
  • 프로그래밍시 import 하지 않아도 자동으로 import된다.
  • import.java.lang.*;
    이런 문장이 추가된다.

Object 클래스

  • 모든 클래스의 최상위 클래스이다.
  • java.lang.Object 클래스
  • 모든 클래스는 Object 클래스에서 상속 받는다.
  • 모든 클래스는 Object 클래스의 메서드를 사용할 수 있다.
  • 모든 클래스는 Object 클래스의 메서드 중 일부를 재정의 할 수 있다. 그러나 final로 선언된 메서드는 재정의 할 수 없다.
  • 컴파일러가 extends Object를 추가한다.

Object 클래스 메서드 중 일부는 다음과 같다. 몇몇 메서드는 밑에서 더 자세히 적어놓았다. Object 클래스 메서드 중에서 final로 선언된 메서드는 하위 클래스에서 재정의할 수 없다.

메서드 설명
String toString() 객체를 문자열로 표현하여 반환한다.
boolean equals(Object obj) 두 인스턴스가 동일한지 여부를 반환한다. 재정의하여 논리적으로 동일한 인스턴스임을 정의할 수 있다.
int hashCode() 객체의 해시 코드 값을 반환한다.
Object clone() 객체를 복사하여 동일한 멤버 변수 값을 가진 새로운 인스턴스를 생성한다.
Class getClass() 객체의 Class 클래스를 반환한다.
void finalize() 인스턴스가 힙 메모리에서 제거될 때 가비지 컬렉터(GC)에 의해 호출된다. 네트워크 연결 해제, 열려 있는 파일 스트림 해제 등을 구현한다.
void wait() 멀티스레드 프로그램에서 사용한다. 스레드를 '기다리는 상태(non runnable)로 만든다.
void notify() wait()메서드에 의해 기다리고 있는 스레드를 살행 가능한 상태(runnable)로 가져온다.

toString() 메서드

  • Object 클래스의 메서드로, 객체의 정보를 String으로 바꾸어서 사용할 때 많이 쓰인다.
  • String이나 Integer 클래스에는 이미 재정의 되어있다. String은 문자열을 반환하고, Integer는 정수 값을 반환한다.

아래 예시를 보자. 똑같이 new를 해서 출력했지만 book과 str의 출력 결과는 다르다. book을 출력했더니  패키지 이름.클래스 이름@ 해시코드 값 의 형태로 나왔다. 그러나 str을 출력하자 문자 그대로 "test"라는 문자열이 나왔다. 왜 이런 결과가 나온걸까? 왜냐하면 String 클래스에 toString() 메서드가 이미 재정의 되어있기 때문이다. 즉, 자기 문자열 자체를 출력하도록 되어있다.

 

class Book{
    String title;
    String author;
	
    Book(String title, String author){
		this.title = title;
		this.author = author;
    }
}

public class ToString {

    public static void main(String[] args) {
		
        Book book = new Book("자바", "홍길동");
        System.out.println(book);
		
        String str = new String("test");
        System.out.println(str);
    }
}
object.Book@1175e2db
test

그렇다면 str처럼 book도 문자열 그대로 출력하고 싶을 수 있다. 이때 아래와 같이 재정의를 하면 된다. 그냥 적어도 되고, 기억이 안난다면 마우스 우클릭 > Source > Override Implement Methods를 하면 재정의할 수 있는 클래스가 보인다. 따라서 아래와 같이 책 이름과 저자를 반환하도록 하면 문자열을 그대로 출력할 수 있다.

class Book{
    String title;
    String author;
	
    Book(String title, String author){
		this.title = title;
		this.author = author;
    }

    @Override
    public String toString() {
		return title + "," + author;
    }
}

public class ToString {

    public static void main(String[] args) {
		
        Book book = new Book("자바", "홍길동");
        System.out.println(book);
		
        String str = new String("test");
        System.out.println(str);
    }
}
자바,홍길동
test

equals() 메서드

  • 두 인스턴스의 주소값을 비교하여 true/false를 반환한다.
  • 재정의하여 두 인스턴스가 논리적으로 동일함의 여부를 반환한다.

두 인스턴스가 '같다'는건 무엇일까? 두 인스턴스가 같다는 것은 같은 메모리라는 것이다. 즉, 물리적으로 같은상태를 '같다'라고 한다. 아래 예시를 보면 studentLee를 생성하고, 이것을 studentLee2에 할당했다. 이런 경우에는 studentLee하고 studentLee2가 가리키는 메모리가 같다. 그래서 이 때 == 연산자를 사용하여 비교하면 true를 반환한다. equals() 메서드를 사용해도 마찬가지로 true를 반환하는데, equals() 메서드의 원형은 == 연산자와 같이 주소가 같은지(즉, 두 개의 인스턴스가 동일한 메모리인지) 확인하기 때문이다.

Student studentLee = new Student(100,"홍길동");
Student studentLee2 = studentLee ;

studentLee

                                  →  힙메모리 [studentID : 100, studentLeeName : 홍길동]

studentLee2 

 

그렇지만 보통 일상에서 '같다'는 의미는 주소가 같은 것을 따지는 것이 아니다. 예를 들어 눈에 보이는 문자열이 같으면 주소를 따지지 않고 같다고 한다. 주소가 다르다고(인스턴스가 다르다고)해도 같은 문자열을  '논리적'으로 두 개가 같다라는 걸 구현하는 방법이 equals() 메서드를 이용한 방법이다. equals() 메서드는 모든 객체의 부모 클래스인 Object에 정의되어 있기 때문에 아래 코드 결과로는 true를 반환한다.

public class EqualsTest {

	public static void main(String[] args) {
		
        String str1 = new String("test"); // 각각의 힙 메모리 주소가 다르다
        String str2 = new String("test");
		
        System.out.println(str1 == str2); 
        // 같은 주소인가? (두 개의 인스턴스가 동일한 메모리인지 판단)
        System.out.println(str1.equals(str2)); 
        // 문자열이 같은가? (학번, 사번과 같은 논리적인 동일성 판단)
    }
}
false
true

새로운 Student 클래스를 만들어 아래와 같이 문자열이 같은 인스턴스를 비교했을 때, == 연산자를 사용하든 equals() 메서드를 사용하든 false를 반환한다. equals()의 원형은 == 연산자와 똑같기 때문이다. 따라서 이럴 때는 equals() 메서드를 재정의해야한다.

class Student{
    int studentID;
    String studentName;
	
    Student(int studentID, String studentName) {
        this.studentID = studentID;
        this.studentName = studentName;
    }	
}

public class EqualsTest {

	public static void main(String[] args) {

        Student std1 = new Student(10001,"Tomas");
        Student std2 = new Student(10001,"Tomas");
        
        System.out.println(std1 == std2);
        System.out.println(std1.equals(std2));
    }
}
false
false

다음은 Student 클래스에 equals() 메서드를 재정의한 코드다. 매개변수로 Object 타입의 obj로 적어서 넘어온 순간 Object 타입으로 변환이 되기 때문에 다시 다운캐스팅을 해야한다. 따라서 먼저 if문으로 Student 타입인지부터 확인한다. Student 타입이 아니라면 확인할 필요가 없기 때문에 바로 false를 반환하도록 했다. 만약 Student 타입이 맞다면 Student 타입으로 다운캐스팅을 한다. 그리고 나의 studentID와 std의 studentID와 같으면 true를 반환하고, 그렇지 않으면 false를 반환한다. 

class Student{
    int studentID;
    String studentName;
	
    Student(int studentID, String studentName) {
        this.studentID = studentID;
        this.studentName = studentName;
    }

    @Override
    public boolean equals(Object obj) {
        if(obj instanceof Student) {
            Student std = (Student)obj;
            if(studentID == std.studentID)
                return true;
            else
                return false;
		}
        return false;
    }
}

public class EqualsTest {

	public static void main(String[] args) {
    
        Student std1 = new Student(10001,"Tomas");
        Student std2 = new Student(10001,"Tomas");
		
        System.out.println(std1 == std2);
        System.out.println(std1.equals(std2));
    }
}
false
true

hashCode() 메서드

  • hash : 정보를 저장, 검색하기 위해 사용하는 자료구조이다.
  • 자료의 특정 값(키 값)에 대해 저장 위치를 반환해주는 해시 함수를 사용한다.
  • 해시 함수는 어떤 정보인가에 따라 다르게 구현된다.
  • hashCode() 메서드는 인스턴스의 저장 주소를 반환한다.
  • 힙 메모리에 인스턴스가 저장되는 방식이 hash이다.

📃 hash?

어떤 자료를 hashtable이라는 곳에 저장하려고 할 때, 이 자료에 대한 특정값(key 값)을 hashfunction이라는 곳에 넣는다. 여기에 key값을 넣으면 주소값(인덱스 값이라고 표현)을 반환해준다. hashCode()가 반환해주는 것은 객체 인스턴스가 저장된 힙 메모리 주소를 반환해주며, 이 메모리를 JVM이 hash라는 방식으로 관리를 한다. hash 알고리즘은 key 값만 알면 자료가 저장된 위치를 금방 찾을 수 있어 검색을 위한 최적의 알고리즘 중 하나이다. 강의에서는 이정도로 설명해주셨는데, 잘 이해가 안가서 여러 블로그를 찾아보았다. 그래도 어렵다. 더 찾아보고 이해한 후 나중에 추가하겠다..

 

아래 코드에서 보면 std1과 std2을 hashCode() 메서드를 사용하여 출력하면 각자 다른 값이 출력된다. 10진수로 각자의 주소값을 보여주고있다. 그러나  str1과 str2을 출력하면 같은 값이 나온다. 왜냐하면 hashCode() 메서드가 재정의 되어있기 때문이다. 재정의와 관련하여 정리한 내용은 다음과 같다. 

  • hashCode()의 반환 값 : 자바 가상 머신(JVM)이 저장한 인스턴스의 주소값을 10진수로 나타낸다.
  • 논리적으로 동일함을 위해 equals() 메서드를 재정의 하였다면, hashCode() 메서드도 재정의하여 동일한 값이 반환되도록 해야한다.
  • String 클래스 : 동일한 문자열의 인스턴스에 대해 동일한 정수가 반환된다.
  • Integer 클래스 : 동일한 정수값의 인스턴스에 대해 동일한 정수값이 반환된다.
class Student{
    int studentID;
    String studentName;
	
    Student(int studentID, String studentName) {
        this.studentID = studentID;
        this.studentName = studentName;
    }

    @Override
    public boolean equals(Object obj) {
        if(obj instanceof Student) {
            Student std = (Student)obj;
            if(studentID == std.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");
        
        Student std1 = new Student(10001,"Tomas");
        Student std2 = new Student(10001,"Tomas");
		
        System.out.println(std1.hashCode());
        System.out.println(std2.hashCode());
		
        System.out.println(str1.hashCode());
        System.out.println(str2.hashCode()); 
        // hashCode()메서드가 재정의 되어있어서 주소가 같음
    }
}
292938459
917142466
3556498
3556498

📃 hashCode() 메서드가 재정의되어있는 상태에서 원래 hashCode 값을 알고싶다면?

아래와 같이  identityHashCode() 메서드를 사용하면 재정의하기 전의 값을 알 수 있다.

System.out.println(System.identityHashCode(std1));
System.out.println(System.identityHashCode(std2));
292938459
917142466

아래는 Student 클래스에서 같은 studentID를 반환하게함으로써 hashCode() 메서드를 재정의한 코드이다. 결과는 다음과 같다. 

class Student{
    int studentID;
    String studentName;
	
    Student(int studentID, String studentName) {
        this.studentID = studentID;
        this.studentName = studentName;
    }

    @Override
    public boolean equals(Object obj) {
        if(obj instanceof Student) {
            Student std = (Student)obj;
            if(studentID == std.studentID)
                return true;
            else
                return false;
		}
        return false;
    }
    
   public int hashCode() { // hashCode() 메서드 재정의
        return studentID;
    }
}

public class EqualsTest {

	public static void main(String[] args) {
		
        Student std1 = new Student(10001,"Tomas");
        Student std2 = new Student(10001,"Tomas");
		
        System.out.println(std1.hashCode());
        System.out.println(std2.hashCode());
		
        System.out.println(str1.hashCode());
        System.out.println(str2.hashCode()); 
    }
}
10001
10001
3556498
3556498

clone() 메서드

  • 객체의 원본을 복제하는데 사용하는 메서드이다.
  • 기본 틀(prototype), 즉 원본을 유지하고 복잡한 생성 과정을 반복하지 않고 여러개를 복제할 때 사용한다.
  • clone() 메서드를 사용하면 객체의 정보(멤버변수의 값)가 같은 인스턴스가 또 생성되는 것으므로 객체 지향 프로그램의 정보 은닉, 객체 보호의 관점에서 위배될 수 있다. (private까지 복제하므로)
  • 따라서 객체의 clone() 메서드 사용을 허용한다는 의미로 cloneable 인터페이스를 반드시 선언해야한다.
class Point{
    int x;
	int y;
	
    public Point(int x, int y) {
		this.x = x;
		this.y = y;
    }
	
    public String toString() {
		return "x=" + x + ", " + "y=" + y;
    }
}

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;
    }
	
    public String toString() {
        return "원점은 " + point + "이고, 반지름은 "
				+ radius + "입니다.";
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        // TODO Auto-generated method stub
        return super.clone();
    }
}

public class ObjectCloneTest {

    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));
		
        System.out.println(circle);
        System.out.println(cloneCircle);
    }
}
917142466
1993134103
원점은 x=10, y=20이고, 반지름은 5입니다.
원점은 x=10, y=20이고, 반지름은 5입니다.

String 클래스

String을 선언하는 두 가지 방법이 있다. 첫 번째로는 new 키워드를 사용하여 인스턴스를 생성하는 방법, 두 번째로는 문자열을 직접 대입하는 방법이 있다. 아래 예시에서 "test"와 같은 것을 문자열 상수라고 하는데, 프로그램에서 사용하는 모든 상수는 상수 풀(constant pool) 이라는 곳에 있게 된다. 다른 말로는 데이터 영역이라고도 하는데, 상수 풀은 처음 프로그램이 메모리에 load 될 때 자리를 잡는다.

str1           →  힙 메모리 ["abc"]

str2

                   →  상수 풀 ["test"]

str3

new키워드를 사용하여 인스턴스를 생성하면 계속 새로 생성이 되지만, 문자열 상수를 직접 가리키면 같은 주소를 가리킨다.

String str1 = new String("abc"); // 생성자의 매개변수로 문자열 생성
String str2 = "test"; // 문자열 상수를 가리키는 방식

new키워드를 사용하여 생성된 인스턴스 str1과 str2를 비교하면 같은 주소인지확인하므로 false를 반환하고, str3, str4와 같이 문자열 상수를 직접 가리키게 하면 두 개는 동일한 메모리를 가리키는 것이 되므로 true를 반환한다.

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); 
        // 문자열 상수를 직접 가리키게 되면 두 개는 동일한 메모리를 가리킴
    }
}
false
true

String 클래스로 문자열 연결하기

  • 한번 생성된 String 값(문자열)은 변하지 않는다. (immutable)
  • 두 개의 문자열을 연결하면 새로운 인스턴스가 생성된다.
  • 문자열 연결을 계속하면 메모리에 gabage가 생길 수 있다.

아래 예시를 보면, 마치 우리 눈에는 Java 뒤에 Android가 붙은 것처럼 보일 수 있다. 그러나 한번 생성된 String 값은 immutable 하기 때문에 붙은 것이 아니다. str1이 JavaAndroid 라는 새로 생성된 문자열을 가리키는 것이다. hash값을 출력해보면 서로 다른 주소 값을 가지고 있다는 것을 알 수 있다.

public class StringTest2 {

    public static void main(String[] args) {
        String str1 = new String("Java");
        String str2 = new String("Android");
		
        System.out.println(System.identityHashCode(str1));
        str1 = str1.concat(str2);
		
        System.out.println(str1);
        System.out.println(System.identityHashCode(str1));
    }
}
2111991224
JavaAndroid
292938459

이렇게 문자열을 연결하려고 하면 실제로 연결된다기보다 메모리가 계속 새로 만들어진다. 따라서 많은 문자열을 연결할 때는 적절하지 않기 때문에 그런 경우에는 StringBuilder, StringBuffer 라는 클래스를 사용한다.

StringBuilder, StringBuffer 사용하여 문자열 연결하기

  • 멤버 변수가 final이 아니다. 즉 immutable하지 않다. 내부적으로 가변적인 char[]배열을 가지고 있는 클래스이다.
  • 문자열을 여러 번 연결하거나 변경할 때 사용하면 유용하다.
  • 매번 새로 생성하지 않고 기존 배열을 변경하므로 gabage가 생기지 않는다.
  • StringBuffer는 멀티 쓰레드 프로그래밍에서 동기화(sychronization)를 지원한다.
  • StringBuilder는 동기화를 지원하지 않아 단일 쓰레드 프로그램에서 사용을 권장한다.
  • toString() 메서드로 String 반환

연결한 상태에서 주소를 찍어보면, String으로 연결할 때와 달리 동일한 주소를 가리키고 있다는 것을 알 수 있다. 그 후 최종적으로 buffer에 toString() 메서드로 String 을 반환한다.

public class StringBuilderTest {

    public static void main(String[] args) {

        String str1 = new String("Java");
        System.out.println(System.identityHashCode(str1));
		
        StringBuilder buffer = new StringBuilder(str1);
        System.out.println(System.identityHashCode(buffer));
		
        buffer.append(" and");
        buffer.append(" Android");
        System.out.println(buffer);
        System.out.println(System.identityHashCode(buffer));
		
        String str2 = buffer.toString();
        System.out.println(str2);
        System.out.println(System.identityHashCode(str2));
    }
}
2111991224
292938459
Java and Android
292938459
Java and Android
917142466

Wrapper 클래스

  • 기본 자료형(primitive data type)에 대한 클래스이다.
기본형 Wrapper 클래스
boolean Boolean
byte Byte
char Character
short Short
int Integer
long Long
float Float
double Double

오토박싱(autoboxing)과 언박싱(unboxing)

아래 코드에서 Integer는 객체고, int는 4바이트 자료형이다. 그래서 예전에는 num1과 num2를 그냥 더할 수 없었다. 이제는 컴파일러가 알아서 두 개의 자료를 같이 연산 할 때 자동으로 변환을 해준다. 정리가 잘 안돼서 검색해보니 기본 자료형을 Wrapper 클래스로 감싸면 박싱, Wrapper 클래스에서 꺼내면 언박싱이라고 한다. 오토 박싱이란 컴파일러가 자동으로 박싱을 해주는 것이다.

Integer num1 = new Integer(100);
int num2 = 200;

int sum = num1 + num2; // num.intValue()로 변환, 언박싱
Integer num3 = num2; // Integer.valueOf(num2)로 변환, 오토박싱

예전에는 new를 해서 쓰는 경우가 많았는데, 지금은 아래와 같이 써도 무방하다.

Integer i = 100;

Class 클래스 

  • 자바의 모든 클래스와 인터페이스는 컴파일 후 class 파일로 생성된다.
  • class 파일에는 객체의 정보(멤버 변수, 메서드, 생성자 등)가 포함되어 있다.
  • Class 클래스는 컴파일된 class 파일에서 객체의 정보를 가져올 수 있다.

어떤 클래스의 정보를 알아올 수 있다. 자료형이 선언되지 않은 클래스의 정보를 알아와서 하는 프로그램 - 리플랙션 프로그램

을 할때 사용하는게 Class 클래스

Class 클래스 가져오기

Class 클래스를 가져오는 방법에는 크게 3가지가 있다. 여기서 꼭 알아야 할 것은 Class.forName("클래스 이름") 메서드를 사용하는 방법이다. 이 메서드는 클래스 이름을 String으로 가져서 클래스를 메모리에 올리는(동적로딩) 역할을 할 수 있는 메서드이다.

  1. Object 클래스의 getClass( ) 메서드 이용하기 : 이미 인스턴스가 있다면 사용. Class 클래스를 반환해주는 일을 함.
  2. 클래스 파일 이름을 Class 변수에 직접 대입하기
  3. Class.forName("클래스 이름") 메서드 사용하기
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;
    }

    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 클래스 사용법 1. getClass() 메서드(Object의 메서드)
        Class pClass1 = person.getClass();
        System.out.println(pClass1.getName());
		
        // CLass 클래스 사용법 2. 클래스 파일 이름을 Class 변수에 직접 대입
        Class pClass2 = Person.class;
        System.out.println(pClass2.getName());
		
        // CLass 클래스 사용법 3. 안에 문자열로 클래스를 쓰고 클래스가 있으면 메모리에 로딩시킴
        Class pClass3 = Class.forName("classex.Person");
        System.out.println(pClass3.getName());
        // classex.Person 라는 이름의 클래스가 있다면 그 클래스를 가져오는 것(동적 로딩) 
	}
}