| 프로그래머스 불량 사용자

https://school.programmers.co.kr/learn/courses/30/lessons/64064

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

코드 설명

불량사용자의 조합이 여러가지 나올 수 있기 때문에 중복을 허용하지 않는 set 을 사용하여 불량사용자 조건에 해당되는 user들을 set에 모두 중복, 순서에 상관없이 add 해주는 방법으로 문제를 구현하였다.

유저 아이디(str1)가 불량사용자의 아이디(str2)에 해당이 되는지 검증하여 true/false 로 나타내는 함수이다.
첫번째 if문에서는 길이를 비교하여 길이가 맞지 않으면 false를 return 해주었고, 그아래 코드에선
각 문자를 비교하여 * 이 아니거나 문자열이 일치하지 않는 경우 false를 return 해주었다.

    public boolean check(String str1, String str2) {
        boolean flag = true;

        if (str1.length() != str2.length()) {
            flag = false;
            return flag;
        }

        for (int i = 0; i < str1.length(); i++) {
            if (str1.charAt(i) != str2.charAt(i) && str2.charAt(i) != '*') {
                flag = false;
                return flag;
            }
        }

        return flag;
    }

여기서 백트래킹 방법으로 HashSet에 다가 해당 되는 userId를 계속 add 해주었다.
유저 수 만큼 for문을 돌려서 해당 유저가 불량 사용자 조건에 해당 되는지를 체크하여 해당된다고 하면 set에 add를 해주었다.

여기서 헷갈렸던 포인트는 
DFS(new HashSet<> (set), depth + 1); // 객체 자체를 매개변수로 넘김
DFS(set, depth + 1); // 객체 주소를 매개변수로 넘김

이거에 따라 테스트케이스 3번이 실패하냐 성공하냐로 나뉘었다는 점이었다.
질문하기 게시판에 있는 댓글을 보고 힌트를 얻어 깊은 복사, 얕은 복사 개념 때문이었다는 것을 알게되었고
오늘도 공부할게 정말 많고 갈길이 멀다는 것을 느꼈다.

https://studywithus.tistory.com/116

 

[JAVA] 깊은 복사(Deep Copy) vs 얕은 복사(Shallow Copy)

안녕하세요! 이번에 정리할 내용은 자바에서의 깊은 복사와 얕은 복사 입니다. 깊은 복사와 얕은 복사라는 개념은 평소에 접한적이 꽤 있었습니다. 하지만 오늘 알고리즘 문제를 풀면서 아무런

studywithus.tistory.com

또한 HashSet<HashSet<String>> result_set = HashS

    public void DFS(HashSet<String> set, int depth) {
        if (depth == bannedid.length) {
            result_set.add(new HashSet<>(set));
            
            return;
        }

        for (int i = 0; i < userid.length; i++) {
            if (set.contains(userid[i]))
                continue;

            if (check(userid[i], bannedid[depth])) {
                set.add(userid[i]);
                
                DFS(new HashSet<> (set), depth + 1);
                // DFS(set, depth + 1);
                set.remove(userid[i]);
            }
        }
    }

HashSet 안에 HashSet 을 이렇게 선언해준 이유는
불량사용자들의 조합을 Set으로 저장했을 때, 입출력예시 3번 처럼
["frodo", "fradi", "crodo", "abc123", "frodoc"]
["fr*d*", "*rodo", "******", "******"]
아래 불량유저 조건에 해당되는 조합이
[crodo, abc123, frodo, frodoc] 
[crodo, abc123, frodo, frodoc] 
[crodo, abc123, frodo, frodoc] [fradi, abc123, frodo, frodoc] 
[crodo, abc123, frodo, frodoc] [fradi, abc123, frodo, frodoc] 
[crodo, abc123, frodo, frodoc] [crodo, fradi, abc123, frodoc] [fradi, abc123, frodo, frodoc] 
이런 식으로 5개 나오지만 1,2번과 3,4번은 각각 중복되는 하나의 조합이기 때문에 
HashSet에 넣어서 중복을 제거해준 것이다.

HashSet<HashSet<String>> result_set = new HashSet<>();

 

전체코드

import java.util.*;

class Solution {

    static String[] userid;
    static String[] bannedid;
    static int answer = 0;
    
     HashSet<HashSet<String>> result_set = new HashSet<>();

    public int solution(String[] user_id, String[] banned_id) {

        userid = user_id;
        bannedid = banned_id;

        DFS(new HashSet<String>(), 0);

        return result_set.size();
    }

    public void DFS(HashSet<String> set, int depth) {
        if (depth == bannedid.length) {
            result_set.add(new HashSet<>(set));
            
            return;
        }

        for (int i = 0; i < userid.length; i++) {
            if (set.contains(userid[i]))
                continue;

            if (check(userid[i], bannedid[depth])) {
                set.add(userid[i]);
                // new HashSet<> (set) 차이가 뭐지
                
                DFS(new HashSet<> (set), depth + 1);
                // DFS(new HashSet<> (), depth + 1);
                set.remove(userid[i]);
            }
        }
    }

    public boolean check(String str1, String str2) {
        boolean flag = true;

        if (str1.length() != str2.length()) {
            flag = false;
            return flag;
        }

        for (int i = 0; i < str1.length(); i++) {
            if (str1.charAt(i) != str2.charAt(i) && str2.charAt(i) != '*') {
                flag = false;
                return flag;
            }
        }

        return flag;
    }
}

안녕하세요! 이번에 정리할 내용은 자바에서의 깊은 복사와 얕은 복사 입니다.

깊은 복사와 얕은 복사라는 개념은 평소에 접한적이 꽤 있었습니다.

하지만 오늘 알고리즘 문제를 풀면서 아무런 의심없이(?) 다음과 같이 컬렉션 List를 얕은 복사하는 코드를 작성했었고, 이에 따라 참조하고 있는 두 리스트가 모두 값이 변경되어 생각했던 아웃풋과 다르게 나와서 약간 어리둥절한 상태였습니다. 🤔

List<String> list = new ArrayList<>();

...

List<String> temp = list; // shallow copy

 

해당 문제점은 디버깅을 통해 파악할 수 있었는데요, 기본적인 내용이지만 확실하게 정리하고 넘어가도록 하겠습니다 😃

 

깊은 복사(Deep Copy)는 '실제 값'을 새로운 메모리 공간에 복사하는 것을 의미하며,

얕은 복사(Shallow Copy)는 '주소 값'을 복사한다는 의미입니다.

 

얕은 복사의 경우 주소 값을 복사하기 때문에, 참조하고 있는 실제값은 같습니다.

예제와 설명을 통해 확인해보겠습니다.

 

 

🎯  얕은 복사(Shallow Copy)

public class CopyObject {

    private String name;
    private int age;
    
    public CopyObject() {
    }

    public CopyObject(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;
    }
}



import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class CopyObjectTest {

    @Test
    void shallowCopy() {
        CopyObject original = new CopyObject("JuHyun", 20);
        CopyObject copy = original; // 얕은 복사

        copy.setName("JuBal");

        System.out.println(original.getName());
        System.out.println(copy.getName());
    }
}

위 코드에서는 copy 객체에 set메소드를 통해 이름을 변경했는데,

실제 결과는 original 객체와 copy 객체 모두 값이 변경이 되었습니다.

 

CopyObject copy = original 의 코드에서 객체의 얕은 복사를 통해 '주소 값'을 변경했기 때문에

참조하고 있는 실제 값은 동일하고, 복사한 객체가 변경된다면 기존의 객체도 변경이 되는 것입니다.

 

 

위 상태에 대한 메모리 구조를 나타내면 다음과 같이 됩니다.

CopyObject original = new CopyObject("JuHyun", 20);
CopyObject copy = original;

스택이 스팸으로 보이는건 배가 고파서일까요..

original 인스턴스를 생성하면 Stack 영역에 참조값이, Heap 영역에 실제값이 저장이 됩니다.

그리고 얕은 복사를 통해 객체를 복사했기 때문에 copy 인스턴스는 original 인스턴스가 참조하고 있는

Heap 영역의 참조값을 동일하게 바라보고 있는 상태가 됩니다.

 

그 후 set 메소드를 통해 값을 변경하면 동일한 주소를 참조하고 있기 때문에 아래와 같이 됩니다.

따라서 코드로는 copy 객체의 name만 변경했지만,

동일한 주소를 참조하고 있기 때문에 original의 객체에도 영향을 끼치게 됩니다.

 

그래서 객체를 출력해보면 아래와 같이 동일한 주소가 출력이 됩니다.

CopyObject original = new CopyObject("JuHyun", 20);
CopyObject copy = original;

System.out.println(original);
System.out.println(copy);

 

 

 

 

🎯  깊은 복사(Deep Copy)

깊은 복사를 구현하는 방법은 여러가지가 있습니다.

  • Cloneable 인터페이스 구현
  • 복사 생성자
  • 복사 팩터리 등등....

 

 

◎ Cloneable 인터페이스 구현

Cloneable 인터페이스는 위와 같이 빈 껍데기의 인터페이스지만

주석을 살펴보면 Object 클래스의 clone() 메소드를 반드시 구현하라고 설명이 되어있습니다.

 

 

public class CopyObject implements Cloneable {

    private String name;
    private int age;

    public CopyObject() {
    }

    public CopyObject(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    protected CopyObject clone() throws CloneNotSupportedException {
        return (CopyObject) super.clone();
    }

    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;
    }
}


    @Test
    void shallowCopy() throws CloneNotSupportedException {
        CopyObject original = new CopyObject("JuHyun", 20);
        CopyObject copy = original.clone();

        copy.setName("JuBal");

        System.out.println(original.getName());
        System.out.println(copy.getName());
    }

깊은 복사를 통해 테스트를 진행해보면 얕은 복사와는 달리 original 인스턴스의 값은 변경이 되지 않습니다.

 

 

※ Effective Java 13장에서는 clone 재정의는 주의해서 진행하라 라는 아이템이 있습니다.

해당 책의 내용을 대략적으로 살펴보면 다음과 같습니다.

Cloneable 인터페이스는 복제해도 되는 클래스임을 명시하는 용도의 믹스인 인터페이스지만, 아쉽게도 의도한 목적을 제대로 이루지 못했다. 여기서 큰 문제점은 clone 메서드가 선언된 곳이 Cloneable이 아닌 OBject이고, 그 마저도 protected이다. 그래서 Cloneable을 구현하는 것만으로는 외부 객체에서 clone 메소드를 호출할 수 없다. 리플렉션을 사용하면 가능하지만, 100% 성공하는 것도 아니다. 

이러한 여러 문제점을 가진 인터페이스이지만, Cloneable 방식은 널리 쓰이고 있어서 잘 알아두는 것이 좋다.

 

Cloneable이 몰고 온 모든 문제를 되짚어봤을 때, 새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안 되며, 새로운 클래스도 이를 구현해서는 안된다. final 클래스라면 Cloneable을 구현해도 위험이 크지는 않지만, 성능 최적화 관점에서 검토한 후 별다른 문제가 없을 때만 드물게 허용해야 한다.

 

기본 원칙은 '복제 기능은 생성자와 팩터리를 이용하는게 최고' 라는 것이다.

단, 배열만은 clone 메소드 방식이 가장 깔끔한, 이 규칙의 합당한 예외라 할 수 있다.

 

http://www.yes24.com/Product/Goods/65551284

 

 

 

◎ 복사 생성자, 복사 팩터리

public class CopyObject {

    private String name;
    private int age;

    public CopyObject() {
    }

    /* 복사 생성자 */
    public CopyObject(CopyObject original) {
        this.name = original.name;
        this.age = original.age;
    }

    /* 복사 팩터리 */
    public static CopyObject copy(CopyObject original) {
        CopyObject copy = new CopyObject();
        copy.name = original.name;
        copy.age = original.age;
        return copy;
    }

    public CopyObject(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;
    }
}


    @Test
    void shallowCopy() {
        CopyObject original = new CopyObject("JuHyun", 20);
        CopyObject copyConstructor = new CopyObject(original);
        CopyObject copyFactory = CopyObject.copy(original);

        copyConstructor.setName("JuBal");
        copyFactory.setName("BalJu");

        System.out.println(original.getName());
        System.out.println(copyConstructor.getName());
        System.out.println(copyFactory.getName());
    }

복사 생성자와 복사 팩터리를 통해 객체를 복사하는 과정도 깊은 복사임을 알 수 있습니다.

 

 

깊은 복사를 그림으로 나타내면 다음과 같습니다.

얕은 복사와는 다르게 Heap 영역에 새로운 메모리 공간을 생성하여 실제 값을 복사합니다.

 

Collections나 Map의 경우 이미 복사 팩터리인 copy() 메소드를 구현하고 있습니다.

    /**
     * Copies all of the elements from one list into another.  After the
     * operation, the index of each copied element in the destination list
     * will be identical to its index in the source list.  The destination
     * list's size must be greater than or equal to the source list's size.
     * If it is greater, the remaining elements in the destination list are
     * unaffected. <p>
     *
     * This method runs in linear time.
     *
     * @param  <T> the class of the objects in the lists
     * @param  dest The destination list.
     * @param  src The source list.
     * @throws IndexOutOfBoundsException if the destination list is too small
     *         to contain the entire source List.
     * @throws UnsupportedOperationException if the destination list's
     *         list-iterator does not support the {@code set} operation.
     */
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }

 

| 자바 getOrDefault 활용법

getOrDefault(Object key, V DefaultValue)

매개 변수 : 이 메서드는 두 개의 매개 변수를 허용합니다.

  • key : 값을 가져와야 하는 요소의 키입니다.
  • defaultValue : 지정된 키로 매핑된 값이 없는 경우 반환되어야 하는 기본값입니다.

반환 값 : 찾는 key가 존재하면 해당 key에 매핑되어 있는 값을 반환하고, 그렇지 않으면 디폴트 값이 반환됩니다.

import java.util.HashMap;

public class Main {
    public static void main(String[] args) {
        HashMap<String, Integer> hm = new HashMap<>();

        for(int i = 0; i < 5; i++){
            hm.put("A", hm.getOrDefault("A", 0) + 1);
            System.out.println(hm.get("A"));
        }

    }
}

해쉬맵을 사용할 때, 키값이 존재하면 매핑되어있는 값을 반환한다는 특징을 통해서 value 가 키값의 카운트 값을 의미하는 경우 우 +1 을 해서 다시 put 해줌으로서 저렇게 활용할 수 있다. 알고리즘 풀 때 자주 응용된다.

| 자바 toCharArray 활용법

 

프로그래머스를 풀다가 자주 사용하게 될 것 같은 함수를 발견하여 정리해둡니다. 

class Main {

    public static void main(String[] args) {
        String s = "Hello World";

        char[] arr = s.toCharArray();

        for(char ch : arr)
            System.out.print(ch+" ");
    }
}

문자열 "Hello World"를 char 배열에 각각 담을 수 있는 함수!
String (문자열) 을 Char형 배열로 바꿔줍니다

결과값

Hello World 가 각각 char 배열에 정상적으로 담긴 것을 확인 할 수 있습니다.

 

    List<String> sortedList = new ArrayList<>(importand_document_entities);

    Comparator<String> c = new Comparator<String>() {
        public int compare(String s1, String s2) {
            return Integer.compare(s2.length(), s1.length());
        }
    };

    Collections.sort(sortedList, c);

 

    // 배열 길이순으로 정
    Arrays.sort(tmp,new Comparator<String>(){
        public int compare(String o1, String o2){

            return Integer.compare(o1.length(), o2.length());
        }
    });

 

반대순으로 정렬은 compare 내부의 o1,o2끼리 위치만 바꿔주면 된다!

Map에 값을 전체 출력하기 위해서는 entrySet(), keySet() 메소드를 사용하면 되는데 entrySet() 메서드는 key와 value의 값이 모두 필요한 경우 사용하고, keySet() 메서드는 key의 값만 필요한 경우 사용합니다.

방법 01 : entrySet()

Map<String, String> map = new HashMap<String, String>();
map.put("key01", "value01");
map.put("key02", "value02");
map.put("key03", "value03");
map.put("key04", "value04");
map.put("key05", "value05");

// 방법 01 : entrySet()
for (Map.Entry<String, String> entry : map.entrySet()) {
	System.out.println("[key]:" + entry.getKey() + ", [value]:" + entry.getValue());
}

 

방법 02 : keySet()

Map<String, String> map = new HashMap<String, String>();
map.put("key01", "value01");
map.put("key02", "value02");
map.put("key03", "value03");
map.put("key04", "value04");
map.put("key05", "value05");
        
// 방법 02 : keySet()
for (String key : map.keySet()) {
	String value = map.get(key);
    System.out.println("[key]:" + key + ", [value]:" + value);
}    

 

Iterator 인터페이스를 사용할 수 없는 컬렉션인 Map에서 Iterator 인터페이스를 사용하기 위해서는 Map에 entrySet(), keySet() 메소드를 사용하여 Set 객체를 반환받은 후 Iterator 인터페이스를 사용하시면 됩니다.

방법 03 : entrySet().iterator()

Map<String, String> map = new HashMap<String, String>();
map.put("key01", "value01");
map.put("key02", "value02");
map.put("key03", "value03");
map.put("key04", "value04");
map.put("key05", "value05");
    
// 방법 03 : entrySet().iterator()
Iterator<Map.Entry<String, String>> iteratorE = map.entrySet().iterator();
while (iteratorE.hasNext()) {
	Map.Entry<String, String> entry = (Map.Entry<String, String>) iteratorE.next();
   	String key = entry.getKey();
   	String value = entry.getValue();
   	System.out.println("[key]:" + key + ", [value]:" + value);
}

 

방법 04 : keySet().iterator()

Map<String, String> map = new HashMap<String, String>();
map.put("key01", "value01");
map.put("key02", "value02");
map.put("key03", "value03");
map.put("key04", "value04");
map.put("key05", "value05");

// 방법 04 : keySet().iterator()
Iterator<String> iteratorK = map.keySet().iterator();
	while (iteratorK.hasNext()) {
	String key = iteratorK.next();
	String value = map.get(key);
	System.out.println("[key]:" + key + ", [value]:" + value);
}

 

방법 05 : Lambda 사용

Map<String, String> map = new HashMap<String, String>();
map.put("key01", "value01");
map.put("key02", "value02");
map.put("key03", "value03");
map.put("key04", "value04");
map.put("key05", "value05");

// 방법 05 : Lambda 사용
map.entrySet().stream().forEach(entry-> {
	System.out.println("[key]:" + entry.getKey() + ", [value]:"+entry.getValue());
});

 

방법 06 : Stream 사용

Map<String, String> map = new HashMap<String, String>();
map.put("key01", "value01");
map.put("key02", "value02");
map.put("key03", "value03");
map.put("key04", "value04");
map.put("key05", "value05");

// 방법 06 : Stream 사용
map.entrySet().stream().forEach(entry-> {
	System.out.println("[key]:" + entry.getKey() + ", [value]:"+entry.getValue());
});
	        
// Stream 사용 - 내림차순
map.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(entry-> {
	System.out.println("[key]:" + entry.getKey() + ", [value]:"+entry.getValue());
});

// Stream 사용 - 오름차순
map.entrySet().stream().sorted(Map.Entry.comparingByKey(Comparator.reverseOrder())).forEach(entry-> {
	System.out.println("[key]:" + entry.getKey() + ", [value]:"+entry.getValue());
});

출처: https://tychejin.tistory.com/31 [너나들이 개발 이야기:티스토리]

Java에서 int와 boolean과 같은 일반적인 데이터 타입의 비교는 ==이라는 연산자를 사용하여 비교합니다. 하지만 String처럼 Class의 값을 비교할때는 ==이 아닌 equals()라는 메소드를 사용하여 비교를 합니다. equals와 == 은 어떤 차이점이 있을까요.

String 변수 생성시 주소할당

String변수를 생성할때는 두가지 방법이 있습니다.

1. 리터럴을 이용한 방식 
2. new 연산자를 이용한 방식 

 

위의 두 가지 방식에는 큰 차이점이 있습니다. 리터럴을 사용하게 되면 string constant pool이라는 영역에 존재하게 되고 new를 통해 String을 생성하면 Heap 영역에 존재하게 됩니다. String을 리터럴로 선언할 경우 내부적으로 String의 intern() 메서드가 호출되게 되고 intern() 메서드는 주어진 문자열이 string constant pool에 존재하는지 검색하고 있다면 그 주소값을 반환하고 없다면 string constant pool에 넣고 새로운 주소값을 반환합니다. 

 

여기서 String pool 이란?

Pool 하면 보통 수영장 풀, 풀장 등을 떠올리실 텐데요. 이처럼 String pool 하면 string이 존재하는 영역을 생각하시면 될 것 같습니다. 같은 String이지만 생성 방식에 따라 차이가 있어 문자열 비교 시 혼란을 주기도 하는데요. 다음 예제를 통해 설명해보겠습니다.

String a = "apple";
String b = new String("apple");
String c = "apple";
String d = new String("apple");

System.out.println(a==b); // false
System.out.prrintln(a==c); // true

 

첫 번째 줄에서 String a가 생성될 때, 'apple'은 리터럴 문자열이므로 Heap영역 중에서 String pool에 위치하게 됩니다.
두 번째 줄에서 String b는 new 연산자를 통해 객체를 생성합니다. Heap영역에 위치하지만 String pool이 아닌 다른 주소값을 참조합니다.

'=='연산자는 같은 메모리를 참조하는가를 비교합니다. 때문에  a와 b의 비교(a==b)는 false를 반환하게 됩니다.

그렇다면 c는 어떨까요? c가 가리키는 'apple'을 String pool에 넣으려고 보니 이미 'apple'이 존재합니다. 그러면 c는 a와 같은 String pool의 'apple'을 가리키게 됩니다.

a와 c가 같은 값을 참조하므로 a==c는 true를 반환합니다.

이를 그림으로 표현해보면 다음과 같습니다.

 

 

주소값 비교(==)와 값 비교(equals)

==연산자와 equals()메소드의 가장 큰 차이점은 == 연산자는 비교하고자 하는 두개의 대상의 주소값을 비교하는데 반해 String클래스의 equals 메소드는 비교하고자 하는 두개의 대상의 값 자체를 비교한다는 것입니다. 기본 타입의 int형, char형등은 Call by Value 형태로 기본적으로 대상에 주소값을 가지지 않는 형태로 사용됩니다. 하지만 String은 일반적인 타입이 아니라 클래스입니다. 클래스는 기본적으로 Call by Reference형태로 생성 시 주소값이 부여됩니다. 그렇기에 String타입을 선언했을때는 같은 값을 부여하더라도 서로간의 주소값이 다릅니다.

 

문자열 비교 (==연산자)

public class compare {
    public static void main(String[] args) {
        String s1 = "abcd";
        String s2 = new String("abcd");
		
        if(s1 == s2) {
            System.out.println("두개의 값이 같습니다.");
        }else {
            System.out.println("두개의 값이 같지 않습니다.");
        }
    }
}

위의 결과를 보시면 ==으로 비교한 두개의 값은 서로 다르다는 결론이 나오게 됩니다. == 연산자의 경우 참조 타입 변수들 간의 연산은 동일한 객체를 참조하는지, 다른 객체를 참조하는지 알아볼 때 사용됩니다. 참조 타입의 변수의 값은 힙 영역의 객체 주소이므로 결국 주소 값을 비교하는 것이 되어 다르다는 결론이 나온것입니다. 그래서 자바에서 문자열을 비교하려면 equals라는 메서드를 활용하여 두개의 값을 비교해주어야 합니다.

 

문자열 비교 (equals메서드)

public class compare {
    public static void main(String[] args) {
        String s1 = "abcd";
        String s2 = new String("abcd");
		
        if(s1.equals(s2)) {
            System.out.println("두개의 값이 같습니다.");
        }else {
            System.out.println("두개의 값이 같지 않습니다.");
        }
    }
}

String 클래스안에 있는 equals라는 메서드를 사용하면 두 비교대상의 주소 값이 아닌 데이터값을 비교하기 때문에 어떻게 String을 생성하느냐에 따라 결과가 달라지지 않고 정확한 비교를 할 수 있습니다.

StringTokenizer 클래스란?

BufferedReader 클래스의 메서드로 입력을 읽어들이면, 라인 단위로 읽어들일 수밖에 없어요.

거기서 머 스페이스 기준으로 문자열을 분리한다던가 필요할때가 많겠죠?

 

BufferedReader 클래스가 아니더라도, 컴마로 구분되는 문자열들을 분리한다던가, 

특정 문자에 따라 문자열을 나누고 싶을 때에 StringTokenizer를 이용하실 수 있습니다. 

 

String : 문자열을

Tokenizer : 토큰화한다.

즉 토큰은 분리된 문자열 조각으로, 스트링토크나이저 클래스는 하나의 문자열을 여러 개의 토큰으로 분리하는 클래스인거죠.

 

StringTokenizer 생성자

StringTokenizer를 생성하는 방식에는 3가지가 있습니다.

생성자 설명
public StringTokenizer(String str); 절달된 매개변수 str을 기본(default) delim으로 분리합니다. 기본 delimiter는 공백 문자들인 " \t\n\r\t"입니다. 
public StringTokenizer(String str,String delim); 특정 delim으로 문자열을 분리합니다.
public StringTokenizer(String str,String delim,boolean returnDelims); str을 특정 delim으로 분리시키는데 그 delim까지 token으로 포함할지를 결정합니다. 그 매개변수가 returnDelims로 true일시 포함, false일땐 포함하지 않습니다.

StringTokenizer 메서드

앞에서 nextToken() 이용해 토큰값을 가져올 수 있다는 건 대충 알았어요.

이 외에 이 클래스에서 지원하는 메서드들은 뭐가 있을까요?

리턴값 메서드명 역할
boolean hasMoreTokens() 남아있는 토큰이 있으면 true를 리턴, 더 이상 토큰이 없으면 false 리턴
String nextToken() 객체에서 다음 토큰을 반환
String nextToken(String delim) delim 기준으로 다음 토큰을 반환
boolean hasMoreElements() hasMoreTokens와 동일한대 엘레먼트보다 토큰으로 된 메서드를 주로 씀
Object nextElement() nextToekn 메서드와 동일하지만 문자열이 아닌 객체를 리턴
int  countTokens() 총 토큰의 개수를 리턴

자주 사용하는 메서드는 hasMoreTokens, countTokens 그리고 nextToken 정도 되겠습니다.

 

StringTokenizer와 Split 차이?

둘 다 모두 문자열 파싱하는데 사용할 수 있습니다.

 

■ StringTokenizer는 java.util에 포함되어 있는 클래스, split는 String클래스에 속해있는 메소드이다.

■ StringTokenizer는 문자 또는 문자열로 문자열을 구분한다면, split는 정규표현식으로 구분합니다. 

■ StringTokenizer는 빈 문자열을 토큰으로 인식하지 않지만 split는 빈 문자열을 토큰으로 인식하는 차이가 있습니다.

■ Stringtokenizer는 결과값이 문자열이라면 split는 결과 값이 문자열 배열입니다. 따라서 StringTokenizer를 이용할 경우, 전체 토큰을 보고 싶다면 반복문을 이용해서 하나하나 뽑을 수 밖에 없어요.

■ 배열에 담아 반환하는 Split는 데이터를 바로바로 잘라서 반환해주는 StringTokenizer보다 성능이 약간 뒤쳐지겠죠? 그러나 데이터양이 많은 경우 거기서 거기기 때문에 크게 신경쓸 필요는 없습니다.

문자열을 다루를 대표적인 클래스 String, StringBuffer, StringBuilder 차이점

연산이 많지 않을때는 위에 나열된 어떤 클래스를 사용하더라도 이슈가 발생할 가능성은 거의 없다. 그러나 연산횟수가 많아지거나 멀티쓰레드, Race condition 등의 상황이 자주 발생 한다면 각 클래스의 특징을 이해하고 상황에 맞는 적절한 클래스를 사용해야 한다.
 

String  vs  StringBuffer/StringBuilder

 
String과 StringBuffer/StringBuilder 클래스의 가장 큰 차이점은 String은 불변(immutable)의 속성을 갖는다는 점입니다.
String str = "hello";   // String str = new String("hello");
str = str + " world";  // [ hello world ]
직관적이어서 가장 많이 사용할 듯한 위의 예제에서 "hello" 값을 가지고 있던 String 클래스의 참조변수 str이 가리키는 곳에 저장된 "hello"에 "world" 문자열을 더해 "hello world"로 변경한 것으로 착각할 수 있습니다.
하지만 기존에 "hello" 값이 들어가있던 String 클래스의 참조변수 str이 "hello world"라는 값을 가지고 있는 새로운 메모리영역을 가리키게 변경되고 처음 선언했던 "hello"로 값이 할당되어 있던 메모리 영역은 Garbage로 남아있다가 GC(garbage collection)에 의해 사라지게 되는 것 입니다. String 클래스는 불변하기 때문에 문자열을 수정하는 시점에 새로운 String 인스턴스가 생성된 것이지요.
위와 같이 String은 불변성을 가지기 때문에 변하지 않는 문자열을 자주 읽어들이는 경우 String을 사용해 주시면 좋은 성능을 기대할 수 있습니다. 그러나 문자열 추가,수정,삭제 등의 연산이 빈번하게 발생하는 알고리즘에 String 클래스를 사용하면 힙 메모리(Heap)에 많은 임시 가비지(Garbage)가 생성되어 힙메모리가 부족으로 어플리케이션 성능에 치명적인 영향을 끼치게 됩니다. (+연산에 내부적으로 char배열을 사용함)
이를 해결하기 위해 Java에서는 가변(mutable)성을 가지는 StringBuffer / StringBuilder 클래스를 도입했습니다.
String 과는 반대로 StringBuffer/StringBuilder 는 가변성 가지기 때문에 .append() .delete() 등의 API를 이용하여 동일 객체내에서 문자열을 변경하는 것이 가능합니다. 따라서 문자열의 추가,수정,삭제가 빈번하게 발생할 경우라면 String 클래스가 아닌 StringBuffer/StringBuilder를 사용하셔야 합니다.
StringBuffer sb= new StringBuffer("hello");
sb.append(" world");

| StringBuffer  vs  StringBuilder

 

그렇다면 동일한 API를 가지고 있는 StringBuffer, StringBuilder의 차이점은 무엇일까요?
가장 큰 차이점은 동기화의 유무로써 StringBuffer는 동기화 키워드를 지원하여 멀티쓰레드 환경에서 안전하다는 점(thread-safe) 입니다.  참고로 String 불변성을 가지기때문에 마찬가지로  멀티쓰레드 환경에서의 안정성(thread-safe)을 가지고 있습니다. 
반대로 StringBuilder는 동기화를 지원하지 않기때문에 멀티쓰레드 환경에서 사용하는 것은 적합하지 않지만 동기화를 고려하지 않는 만큼 단일쓰레드에서의 성능은 StringBuffer 보다 뛰어납니다.
 

| 정리

 
마지막으로 각 클래스별 특징을 정리해 보겠습니다. 컴파일러에서 분석 할때 최적화에 따라 다른 성능이 나올 수도 있지만 일반적인 경우에는 아래와 같은 경우에 맞게 사용하시면 될 것 같네요.
 
String                :  문자열 연산이 적고 멀티쓰레드 환경일 경우
StringBuffer     :  문자열 연산이 많고 멀티쓰레드 환경일 경우
StringBuilder   :  문자열 연산이 많고 단일쓰레드이거나 동기화를 고려하지 않아도 되는 경우  
String, StringBuffer, StringBuilder 비교

* StringBuffer와 StringBuilder는 성능으로 따졌을 때 2배의 속도차이가 있다고 하지만 참고사이트의 속도 차이 실험 결과 append()연산이 약 1억6천만번 일어날 때 약 2.6초의 속도차이를 보인다고 합니다.

(String은 +연산이 16만번이상 넘어가게 되면 10초이상 걸리면서 못 쓸정도의 성능을 보입니다.)

따라서 문자열연산이 많지만 엄청나게 일어나지 않는 환경이라면 StringBuffer를 사용해서 thread-safe한 것이 좋다는 생각입니다.

* JDK1.5이상부터 String에서 +연산으로 작성하더라도 StringBuilder로 컴파일하게 만들어 놨다지만 여전히 String클래스의 객체 생성하는 부분을 동일하므로 StringBuffer,StringBuilder 사용이 필요함.

+ StringBuffer, StringBuilder의 경우 buffer size를 초기에 설정해야하는데 이런 생성, 확장 오버로드가 걸려 버퍼사이즈를 잘못 초기화할 경우 성능이 좋지 않을 수 있음.

+ String클래스가 컴파일러분석단계에서 최적화될 가능성이 있기때문에 간혹 성능이 잘나오는 경우도 있음. 문자열 연산이 많지 않은 경우는 그냥 사용해도 무방.

 

| JAVA Collection Framework의 상속 기본 구조

 

List, Map, Set에 관해 설명드리기 전에 컬렉션 프레임워크를 사용하는 이유에 대해 말씀드리자면, 기존에는 많은 데이터를 처리하기 위해 배열을 사용했었지만 크기가 고정되어있고 삽입 및 삭제 시간이 오래 걸린다는 불편한 점들이 많았습니다. 따라서 이를 보완하기 위해 자바에서 동적 배열 개념인 컬렉션 프레임워크를 제공하였는데 종류는 대표적으로 List, Map, Set이 있습니다. 그리하여 자바 컬렉션 프레임워크로 인해 자료의 삽입, 삭제, 검색 등등이 용이해지고 어떠한 자료형이라도 담을 수 있으며 크기가 자유롭게 늘어난다는 강점을 가져 많은 사람들에게 사용되고 있습니다.

 


| List

 

순서가 있고 중복을 허용합니다.
인덱스로 원소에 접근이 가능합니다.
크기가 가변적입니다.

List의 종류와 특징

  • LinkedList
    • 양방향 포인터 구조로 데이터 삽입, 삭제가 빠르다.
    • ArrayList보다 검색이 느리다. 
  • ArrayList
    • 단반향 포인터 구조로 데이터 순차적 접근에 강점을 가진다.
    • 배열을 기반으로 데이터를 저장한다.
    • 데이터 삽입, 삭제가 느리다.
    • 데이터 검색이 빠르다.

| Map

 

Key와 Value의 한쌍으로 이루어지는 데이터의 집합.
Key에 대한 중복이 없으며 순서를 보장하지 않습니다.
뛰어난 검색 속도를 가집니다.
인덱스가 따로 존재하지 않기 때문에 iterator를 사용합니다.

Map의 종류와 특징

  • HashMap
    • Key에 대한 중복이 없으며 순서를 보장하지 않는다.
    • Key와 Value 값으로 NULL을 허용한다.
    • 동기화가 보장되지 않는다.
    • 검색에 가장 뛰어난 성능을 가진다.
  • HashTable
    • 동기화가 보장되어 병렬 프로그래밍이 가능하고 HashMap 보다 처리속도가 느리다.
    • Key와 Value 값으로 NULL을 허용하지 않는다.
  • LinkedHashMap
    • 입력된 순서를 보장한다.
  • TreeMap
    • 이진 탐색 트리(Red-Black Tree)를 기반으로 키와 값을 저장한다.
    • Key 값을 기준으로 오름차순 정렬되고 빠른 검색이 가능하다.
    • 저장 시 정렬을 하기 때문에 시간이 다소 오래 걸린다.

| Set

 

데이터의 집합이며 순서가 없고 중복된 데이터를 허용하지 않습니다.
중복되지 않은 데이터를 구할 때 유용합니다.
빠른 검색 속도를 가집니다.
인덱스가 따로 존재하지 않기 때문에 iterator를 사용합니다.

Set의 종류와 특징

  • HashSet
    • 인스턴스의 해시값을 기준으로 저장하기 때문에 순서를 보장하지 않는다.
    • NULL 값을 허용한다.
    • TreeSet보다 삽입, 삭제가 빠르다.
  • LinkedHashSet
    • 입력된 순서를 보장한다.
  • TreeSet
    • 이진 탐색 트리(Red-Black Tree)를 기반으로 한다.
    • 데이터들이 오름차순으로 정렬된다.
    • 데이터 삽입, 삭제에는 시간이 걸리지만 검색, 정렬이 빠르다.

 

 

| 정리

 

List는 기본적으로 데이터들이 순서대로 저장되며 중복을 허용한다.

Map은 순서가 보장되지 않고 Key값의 중복은 허용하지 않지만 Value값의 중복은 허용된다.

Set은 순서가 보장되지 않고 데이터들의 중복을 허용하지 않는다.

+ Recent posts