C++에서 배우는 자바-유용한 클래스

java에는 최상위 클래스인 Object부터 해서 수많은 클래스들이 있고 사용방법을 안다면 굉장히 유용하게 쓸 수 있다. 그러한 클래스들을 하나씩 정리해보자.

Object 클래스

java의 최상위 클래스이다. 모든 클래스는 Object 클래스의 자식이고 그렇기에 Object 클래스의 서브 메서드들을 사용 가능하다. 그 중 자주 쓰는 메서드들을 중심으로 살펴보자.

equals()

참조변수를 매개변수로 받아 두 참조변수가 같은 값인지를 boolean 값으로 반환하는 메서드이다.

public static void main(String args[]) {
    Value v1=new Value(10);
    Value v2=new Value(10);
    System.out.println(v1.equals(v2));
    v1=v2;
    System.out.println(v1.equals(v2));
}

위와 같이 작성하면 false,true가 나온다. 같은 값을 가진 인스턴스이지만 new를 사용해 생성한 서로 다른 객체이기에 첫번째 값은 false, 두 참조변수의 값을 같게 해주면 두번째 값은 true가 나오는 것이다.
매우 간단해보이지만 문제는 여러 클래스가 이 equals() 메서드를 오버라이딩하여 사용중이라는 것이다.
대표적으로 String 클래스는 equals() 메서드를 자체적으로 오버라이딩 하여 사용중이다. 참조변수가 아닌 String 인스턴스의 문자열 값을 비교하여 boolean 값을 반환한다.

toString()

인스턴스의 정보를 문자열로 반환한다. String과 Date 객체라면 각각 문자열 내용과 시간을 문자열로 반환하고, 일반 클래스의 경우엔 ‘클래스 이름@해시코드’의 형식으로 반환한다. 위의 eqauls()도 그렇지만 우리는 이 Object 클래스의 메서드들을 오버라이딩하여 원하는 결과를 얻도록 구현할 수 있다. 예를 들어보자.

class Card {
    String kind;
    int num;

    public String toString() {
        return "Kind : " + kind + " num : " + num;
    }
}

위와 같이 toString 메서드를 오버라이딩 한다면 원래는 Card.toString()을 하면 ‘Card@해시코드’와 같은 값을 반환받겠지만 이제는 ‘Kind : kind값 num : num값’과 같이 원하는 값을 반환받을 수 있게 되었다.

clone()

현재 인스턴스를 복사하여 새로운 인스턴스를 생성하는 메서드이다.
clone()의 기본 접근 제어자는 protected이기 때문에 상속 관계에 있는 클래스만이 사용 가능하다. 또한 Cloneable 인터페이스를 구현해야만 해당 클래스의 clone()을 통한 복제가 가능하다.
Cloneable 인터페이스라고 하면 내부에 clone() 추상 메서드가 있을 것 같은 느낌이 들지만 Cloneable 인터페이스는 내부에 내용이 없고 복제가 가능한지 아닌지만을 나타내는 마커 인터페이스이다.
clone()을 상속관계가 없는 클래스에서도 사용하기 위해서는 public으로 오버라이딩 해줘야 한다. 예시는 다음과 같다.

  
public class tmp implements Cloneable {
    public Object clone() {
        try {
            Object obj=super.clone();
        } catch(CloneNotSupportedException e) {}
        return obj;
    }
}

public class App {
    public static void main(String[] args) {
        tmp t=new tmp();
        tmp t2=t.clone();
    }
}

Cloneable 인터페이스를 구현한 클래스 tmp 내에서 clone() 메서드를 public으로 오버라이딩 해주었다. 따라서 전혀 관계가 없는 App 클래스 내에서도 clone() 메서드를 통한 복제가 가능해졌다.

얕은 복사 vs 깊은 복사

위의 clone() 메서드를 공부하다 알게 된 개념이다. clone() 메서드는 인스턴스의 값을 그대로 복제하여 새로운 인스턴스에 입력한다. 그런데 만약 복제해야 할 인스턴스의 값 중에 참조변수가 있다면 어떻게 될까?

  
public class tmp implements Cloneable {
    Point P;
    int i=10;

    public Object clone() {
        try {
            Object obj=super.clone();
        } catch(CloneNotSupportedException e) {}
        return obj;
    }
}

public class App {
    public static void main(String[] args) {
        tmp t=new tmp();
        tmp t2=(tmp) t.clone();
    }
}

아까의 예시에서 참조변수 P와 int형 멤버변수를 추가시켜 보았다. 이 때 t와 t2의 P는 다를까? 다르지 않다. 두 참조변수는 같은 값으로 하나의 객체를 가리키고 t의 P를 변경하면 t2의 P 또한 영향을 받는다. 이러한 복제를 얕은 복사(Shallow Copy)라고 한다.
만약 참조변수까지 새로 복사되게 하려면 어떻게 해야할까? 다음의 코드에서 확인할 수 있다.

  
public class tmp implements Cloneable {
    Point P;
    int i=10;

    public tmp deepcopy() {
        try {
            Object obj=super.clone();
        } catch(CloneNotSupportedException e) {}
        tmp t=(tmp)obj;
        t.P=new Point(this.P.x,this.P.y);
        return t;
    }
}

public class App {
    public static void main(String[] args) {
        tmp t=new tmp();
        tmp t2=t.clone();
    }
}

위와 같이 변경하면 참조변수가 서로 다른 객체를 참조하는 깊은 복사(Deep Copy)가 가능하다. 또한 메서드 내부에서 형변환을 해주기 때문에 사용자는 형변환이 따로 필요없도록 하였다.

getClass()

Class 객체는 각 클래스마다 1개만 존재하고, 프로그램 실행 시 클래스 파일이 메모리에 올라갈 때 자동으로 생성된다. 이러한 Class 객체는 해당 클래스에 대한 모든 정보가 담겨있는데 getClass() 메서드를 사용하면 이러한 Class 객체에 참조할 수 있다. 사용 예는 다음과 같다.

  
public class tmp {
    Point P;
    int i=10;
}

public class App {
    public static void main(String[] args) {
        Class t=new tmp().getClass();
        tmp t2=t.newInstance();
    }
}

getClass() 메서드를 이용하여 Class 객체에 대한 참조를 받아온 후 Class 객체를 이용하여 newInstance() 메서드로 새 인스턴스를 생성한 코드이다.

String

문자열을 다루는 부분은 어떤 언어이든지 대단히 중요하다. String이 변경 불가능한 클래스라는 것, 리터럴로 초기화 시 같은 값의 객체가 이미 있다면 그것을 가리키는 것 등 String 클래스의 특징에 대해선 이전에 얘기한 것이 있으니 넘어가고 중요한 서브 메서드들을 중심으로 다뤄보도록 하겠다.

split & join

split과 join을 이용하면 문자열을 구분자를 기준으로 나눌 수도, 합칠 수도 있다. 사용 예는 다음과 같다.

  
public class App {
    public static void main(String[] args) {
        String animals="dog,cat,bear";
        String arr[]=animals.split(",");
        String str=String.join("-",arr);
        System.out.println(str);
    }
}

위의 코드를 실행하면 “dog-cat-bear”라는 문자열이 출력된다. ,를 구분자로 하여 split으로 문자열을 나눈 뒤, join을 이용하여 -를 구분자로 합친 것이다.
java.util.StringJoiner 클래스를 사용하여 합칠 수도 있다.

  
public class App {
    public static void main(String[] args) {
        String arr[]={"dog","cat","bear"};
        StringJoiner sj=new StringJoiner("," , "[" , "]");
        for(String s : arr) {
            sj.add(s);
        }
        System.out.println(sj.toString());
    }
}

위의 코드를 실행하면 “[dog,cat,bear]”라는 문자열을 얻을 수 있다. 시작 문자, 끝 문자, 구분자를 지정하여 문자열을 합칠 수 있는 것이다.

String의 형 변환

알고리즘 문제를 풀다보면 String을 기본형으로, 기본형을 String으로 바꿔야 할 일이 많다. 기본형을 String으로 바꾸는 것은 java에서 아주 간단하다.

  
public class App {
    public static void main(String[] args) {
        int i=100;
        String s=i+"";
        String s1=String.valueOf(i);
    }
}

valueOf() 메서드를 사용하거나 빈 문자열을 더해주면 된다. valueOf() 메서드를 사용하는 것이 약간 더 성능이 좋다.
String을 기본형으로 바꾸는 것도 간단하다.

  
public class App {
    public static void main(String[] args) {
        String s="123";
        int i=Integer.valueOf(s);
    }
}

형에 맞는 wrapper 클래스의 valueOf() 메서드를 사용해주면 된다. wrapper 클래스는 기본형을 클래스로 사용하기 위해 만들어진 것으로 추후 설명하도록 하겠다.

StringBuffer & StringBuilder

C++에서와 달리 java의 String 클래스는 수정이 불가능하고 이는 대단히 불편하다. 따라서 java에는 수정이 가능하고, C++의 vector와 같이 가변배열로써 동작하는 StringBuffer 클래스가 존재한다. 사용 예는 다음과 같다.

  
public class App {
    public static void main(String[] args) {
        StringBuffer sb=new StringBuffer(10);
        sb.append("Hello");
        StringBuffer sb2=new StringBuffer("Hello");
        sb.append(" World");
        System.out.println(sb.equals(sb2));
        Syster.out.println(sb);
        sb.deleteCharAt(0);
        System.out.println(sb);
    }
}

위의 코드의 실행 결과는 false “Hello World” “ello World” 이다.
StringBuffer는 C++의 vector와 대단히 유사하다. 선언할 때 크기를 정할 수 있고, 이 크기를 넘는 문자열이 들어올 경우 복사하여 더 큰 버퍼의 StringBuffer로 이동시킨다. 이는 vector의 재할당과 매우 유사하다.
append() 메서드 사용 시 vector의 push_back()과 마찬가지로 문자열의 맨 뒤에 내용을 저장한다. 예시에서 두 문자열이 붙어 “Hello World”가 된 것을 볼 수 있다.
deleteCharAt() 메서드로 인덱스의 문자를 지울 수도 있다. 그런데 분명 sb와 sb2는 같은 값을 가진 문자열인데 왜 equals() 메서드가 false를 반환한걸까?
StringBuffer는 String 클래스와 달리 equals() 메서드가 오버라이딩 되어있지 않기 때문이다. 따라서 문자열 값에 따라 비교를 하고싶다면 StringBuffer를 toString() 메서드를 통해 String 클래스로 변환 후 비교를 해야 한다.

StringBuiler 클래스는 StringBuffer 클래스에서 동기화 파트만 제거한 것이다. StringBuffer는 멀티쓰레딩 환경에서 thread safe를 보장하도록 설계되었는데, 멀티쓰레딩 환경이 아닐 경우 이로 인해 성능이 저하되는 문제가 있었다. 따라서 이 부분이 제거된 StringBuilder 클래스가 나왔는데, 모든 StringBuffer 사용 예에서 StringBuffer만 StringBuilder로 바꿔주면 동작할 정도로 유사하다.

Wrapper 클래스

간혹 기본형을 사용하는데도 객체가 필요할 때가 있다. 매개변수로 객체만을 받을 때, 객체 간의 비교가 필요할 때 등이 있는데, 이를 위해 기본형을 객체화한 래퍼 클래스(Wrapper Class)가 존재한다.
int와 char를 제외하면 각 자료형의 첫 글자를 대문자로 한 것이 래퍼 클래스이고, int는 Integer, char는 Character 이다. 생성자로는 해당 기본형, 또는 문자열을 받는다.

  
public class App {
    public static void main(String[] args) {
        Integer i=new Integer(3);
        Integer i1=new Integer("3");
        int i2=3;
        System.out.println(i+i2);
    }
}

위의 두 줄은 같은 결과를 갖는다. 다만 문자열로 초기화할 경우 형이 맞지 않으면 예외를 반환하는 것에는 주의해야 한다.
java에선 원래 기본형과 참조형 간의 연산이 불가능하였지만 오토박싱 & 언박싱 기능이 추가되었기 때문에 컴파일러에서 자동으로 래퍼 클래스, 또는 기본형을 형 변환해주어 상호간의 연산이 가능하다.

정규식 & Scanner

java에선 물론 정규식을 사용한 검색을 지원한다. 방식은 다음과 같다.

  1. 정규식을 Pattern의 static 메서드인 compile()을 사용하여 Pattern 인스턴스를 얻는다 (Patter p=Pattern.complie(“c[a-z]*”);)
  2. 비교할 대상을 매개변수로 Pattern의 matcher 메서드를 호출하여 Matcher 인스턴스를 얻는다. (Matcher m=p.matcher(data[i]);)
  3. Matcher 인스턴스에 boolean matches() 메서드를 호출하여 정규식에 부합하는지 확인한다. (if(m.matches()))

이외에도 Matcher의 find() 메서드를 활용, 긴 문자열에서 정규식에 부합하는 부분의 인덱스를 start(), end() 메서드를 이용하여 알아낼 수 있다.

Scanner의 경우 java에서 입력을 받을 때 사용하는 클래스이다. 정규식과 같이 설명하는 이유는 Scanner에서, 그리고 String에서 구분자를 사용할 때 정규식을 사용함으로써 복잡한 구분자도 처리할 수 있기 때문이다.
기본적으로 Scanner는 nextLine() 메서드로 String을 줄 단위로 입력받는다. 보통 boolean을 반환하는 hasNextLine() 메서드로 입력받을 줄이 더 있는지를 확인하여 사용한다.
nextLine() 이외에도 nextInt(), nextDouble() 등의 메서드로 원하는 형을 입력받음으로써 불필요한 형 변환을 막을 수 있다. 사용 예는 다음과 같다.

  
public class App {
    public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        while(sc.hasNextLine()) {
            String input=sc.nextLine();
            String arr[]=input.split(" +");
        }
    }
}

hasNextLine() 메서드를 통해 다음 줄이 있는지 확인한 후, 받아온 줄을 정규식을 활용하여 하나 이상의 공백을 구분자로 쪼개 배열에 저장하는 코드이다.

이로써 Java의 정석 1권 분량이 끝났다. 1권은 기초적인 내용을 주로 다루고 있어 C++과 겹치는 내용이 많아 넘어간 분량이 꽤 되는 것 같음에도 공부하는데 시간이 상당히 걸린 것 같다.
좀 더 심화적인, 그리고 C++과 겹치지 않는 부분이 많이 나올 2권은 얼마나 걸릴 지 모르겠다. 회사에 지원하며, 그리고 면접을 보며 회사에서 어느정도 깊이 있는 지식을 원한다고 느껴 깊이 있는 공부를 위해 부트캠프 등도 가지 않고 있지만 지금 하는 공부가 깊이 있게 하고 있는 것인지 잘 모르겠다.
일단은 열심히 배운 후 프로젝트로 포트폴리오부터 하나 구현해봐야 할 것 같다.