2021. 1. 10. 03:36ㆍ코딩 테스트/개념 학습 및 정리
1. 문자열
- String
- StringBuffer
- StringBuilder
String, StringBuffer, StringBuilder 클래스의 가장 큰 차이점은 String은 불변의 속성을 갖는다는 점이다. 즉, String 클래스는 불변하기 때문에 문자열을 수정하는 시점에 새로운 String 인스턴스가 생성이된다.
변하지 않는 문자열을 자주 읽어들이는 경우 String을 사용하면 좋은 성능을 기대할 수 있으나, 문자열 추가, 수정, 삭제 등의 연산이 빈번하게 발생하는 알고리즘에 String 클래스를 사용하면 힙 메모리에 많은 임시 가비지가 생성되어 힙 메모리 부족으로 애플리케이션 성능에 치명적인 영향을 끼치게 된다.
이를 해결하기 위해서 Java는 가변성을 가지는 StringBuffer와 StringBuilder 클래스를 지원한다. 가변성을 지니기 때문에 append(), delete() 등의 API를 이용하여 동일 객체 내에서 문자열을 변경하는 것이 가능하다. 따라서 문자열의 추가, 수정, 삭제가 빈번하게 발생하는 경우 String 클래스가 아닌 StringBuffer, StringBuilder 클래스를 이용해야한다.
String str = "hello";
str = str + "world";
StringBuffer와 StringBuilder의 차이는, 동기화 유무이다. StringBuffer는 동기화 키워드를 지원하여 멀티쓰레드 환경에서 안전하다는 점이다. (String도 불변성을 지니기에 멀티쓰레드 환경에서의 안정성을 지닌다.) 반면, StringBuilder는 동기화를 지원하지 않기 때문에 멀티쓰레드 환경에서 사용하는 것은 적합하지 않지만 동기화를 고려하지 않는 단일 쓰레드에서의 성능은 StringBuilder 보다 뛰어나다.
클래스 | 설명 |
String | 문자열 연산이 적고 멀티 쓰레드 환경일 경우 |
StringBuffer | 문자열 연산이 많고 멀티 쓰레드 환경일 경우 |
StringBuilder | 문자열 연산이 많고 단일 쓰레드이거나 동기화를 고려하지 않아도 되는 경우 |
※ String을 비교할 때 대소문자를 구분하여 비교하는 행위는 가급적으로 피하는 것이 좋다.
※ 한 문자를 체크하기 위해 startsWith 보다 charAt을 사용하는 것이 좋다.
2. 문자열 분리
- StringTokenizer
- Split
- SubString
Split은 정규 표현식 사용이 가능하여 다양하게 쪼갤 수 있지만 속도가 느리다. 반면 StringTokenizer는 다량의 데이터를 처리하는 속도가 빠르지만 정규 표현식이 사용할 수 없다(원패턴).
3. 입출력
- Scanner
- System.out.println
- BufferedReader, BufferedWriter
일반적으로 많은 사람들이 접하는 방식은 Scanner이다. 하지만 많은 양의 데이터를 입력받을 경우 BufferedReader를 통해 입력받는 것이 효율면에서 좋다. Read한 데이터는 Line 단위로만 나눠지기에 공백단위로 데이터를 가공하려면 따로 작업(StringTokenizer, Split)을 해주어야한다.
System.out.println도 Scanner와 마찬가지로 많은 사람들이 접하는 방식이다. 적은 양의 출력일 경우 성능 차이가 미미하겠지만 많은 양의 출력에서는 입력과 마찬가지로 BufferedWriter를 사용해주는게 좋다.
package org.example;
import java.io.*;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
bw.write(st.nextToken()); // 출력
bw.newLine(); // 줄바꿈
bw.write(st.nextToken()); // 출력
bw.flush(); // 남아있는 데이터 모두 출력
br.close(); // 스트림 닫기
bw.close(); // 스트림 닫기
}
}
4. 캐스팅
1) 문자에서 숫자로
- Integer.parseInt() / Integer.valueOf
- Double.valueOf() / Float.valueOf()
- Long.parseLong() / Short.parseShort()
2) 숫자에서 문자로
- String.valueOf() / Integer.toString()
- String.valueOf() / Float.toString()
- String.valueOf() / Double.toString()
3) 정수에서 실수로, 실수에서 정수로
'(자료형)' 캐스팅 방식으로 변환할 수 있다.
4) 다형성
- IS-A
- HAS-A
다형성은 하나의 코드가 여러 자료형으로 구현되어 실행되는 것을 의미하며 이는 같은 코드에서 여러 실행 결과가 나올 수 있다는 것을 의미한다. 정보 은닉, 상속과 더불어 객체지향 프로그래밍의 가장 큰 특징 중 하나인 다형성은 객체지향 프로그래밍의 유연성, 재활용성, 유지보수성에 기본이 되는 특징이다.
5. Builder
일반적으로 Setter를 이용한 주입은 실무에서도 어렵지않게 확인할 수 있다. 하지만 이는 매우 단순하고 객체 생성 시점에 필요한 모든 값들을 주입하지 않아 개발자의 실수가 발생할 수 있으며, public으로 공개해놓은 set 메서드는 코드 다른 부분에서 언제 호출되어 값이 변경이 될지 알기 힘들다. 따라서 Getter와 Setter는 객체지향적인 코드작성에 가장 큰 적이라고 할 수 있다.
매개변수가 존재하는 생성자의 경우, 필드 순서를 외우지 못하면 객체를 만들 때마다 어떤 필드에 어떤 값을 넣어주어야 하는지 확인해야하는 번거로움이 있다.
이러한 불편함을 해결하기 위해서 Builder 패턴을 사용하면 좋다. 단, 객체를 생성한 후 수정해야하는 경우에 빌더 패턴을 적용해버리면 결국 setter도 함께 사용하게 된다. 따라서 적절한 상황에 맞게 사용하는 것이 가장 좋다.
class Team {
private String name;
private int playCount;
private int victoryPoint;
private int winCount;
private int drawCount;
private int loseCount;
private int scorePoint;
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String name;
private int playCount;
private int victoryPoint;
private int winCount;
private int drawCount;
private int loseCount;
private int scorePoint;
private Builder() {
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder playCount(int playCount) {
this.playCount = playCount;
return this;
}
public Builder victoryPoint(int victoryPoint) {
this.victoryPoint = victoryPoint;
return this;
}
public Builder winCount(int winCount) {
this.winCount = winCount;
return this;
}
public Builder drawCount(int drawCount) {
this.drawCount = drawCount;
return this;
}
public Builder loseCount(int loseCount) {
this.loseCount = loseCount;
return this;
}
public Builder scorePoint(int scorePoint) {
this.scorePoint = scorePoint;
return this;
}
public Team build() {
Team team = new Team();
team.name = this.name;
team.playCount = this.playCount;
team.victoryPoint = this.victoryPoint;
team.winCount = this.winCount;
team.drawCount = this.drawCount;
team.loseCount = this.loseCount;
team.scorePoint = this.scorePoint;
return team;
}
}
}
Team team = Team.builder()
.name("맨유")
.playCount(3)
.victoryPoint(6)
.winCount(2)
.drawCount(0)
.loseCount(1)
.scorePoint(4)
.build();
6. TDD
import org.junit.Assert;
import org.junit.Test;
public class TestClass {
boolean solution(String s) {
boolean answer = true;
return true;
}
@Test
public void 정답() {
TestClass t = new TestClass();
Assert.assertEquals(true, t.solution("test"));
// Assert.assertArrayEquals(new int[] {1,2,3,4,5}, t.solution(1, new int[] {1,2,3,4,5}));
}
}
7. 그 외
1) 자료형, 범위, 연산자
효율적인 코드를 위해서라면, 자료형을 적절하게 활용해야하며, 같은 기능이더라도 증감연산자를 사용하는 것이 좋다.
1 Byte = 8 bits = 256 개
- 1 bit = 0, 1
- 2 bit = 00, 01, 10, 11
- 3 bit = 000, 001, 010, 011, 100, 101, 110, 111
- ...
자료형 | 데이터 | 크기 |
boolean | 참, 거짓 | 1 바이트 |
char | 문자 | 2 바이트 |
byte | 정수 | 1 바이트 |
short | 정수 | 2 바이트 |
int | 정수 | 4 바이트 (32 비트) |
long | 정수 | 8 바이트 |
float | 실수 | 4 바이트 |
double | 실수 | 8 바이트 |
※ 지역 메서드 변수가 스택 영역에 저장되므로 가장 빠르다.
i = i + 1; // 비효율적인 방법
i++; // 효율적인 방법
sum = sum + 10; // 비효율적인 방법
sum += 10 (o) // 효율적인 방법
2) 무분별한 객체 생성 자제
객체는 초기화되면 모두 힙 메모리 영역에 할당이 된다. 만약 무분별하게 객체를 생성할 경우, 로드 및 해제할 때 Garbage Collection 대상이 많아진다는 점이 있다. 따라서 반복문내에 객체를 생성하지 않도록 해야한다. 또한 인스턴스 변수는 클래스 초기화 시 자동적으로 값이 할당(숫자는 0, boolean은 false)되므로 의도적으로 값을 설정(String str = null;)할 필요가 없다.
3) 무분별한 메서드 호출
- static, final 키워드 사용
메서드를 호출한다는 것은 메서드가 위치한 메모리를 참조한다는 의미이다. 특히 자바는 객체지향언어이기 때문에 동적인 메서드의 호출이 발생한다. 즉, 메서드 오버로딩의 경우 정해진 메서드를 부르는게 아닌 그 때 상황에 따라 판단하여 호출이 이루어지므로 될 수 있다면 적게 호출하는 방향으로 로직을 구현하는게 좋다. 따라서 자주 사용하는 메서드는 Static으로 선언하고, 자주 참조하는 변수는 final 키워드로 선언하여 메모리에 고정시켜 성능을 높일 수 있도록 하는게 좋다.
※ 오버로딩 : 이름은 같지만 매개변수가 다를 때 여러 메서드나 생성자가 존재할 수 있다.
※ 오버라이딩 : 상위 클래스에서 정의된 메서드의 구현 내용이 하위 클래스에서 구현할 내용과 맞지 않을 때 사용한다.
4) 동기화
동기화는 쓰레드가 하나의 자원을 놓고 싸우지 말고 락을 가진 쓰레드부터 차례로 그 자원을 쓰고 다음 쓰레드에게 락과 함께 자원을 반납하는 것이다. 이 때 동기화를 안해도 되는 부분을 동기화를 하게 된다면 쓰레다가 하나빡에 수행되지 못하기에 조심해야한다. 동기화 방법은 메서드를 동기화하거나 블락을 동기화할 수 있는데, 동기화된 메서드를 호출하는 것이 동기화된 블록을 호출하는 것보다 약간 빠르다. 동기화된 메서드에서 동기화된 다른 메서드를 부를 경우에는 호출되는 메서드를 private 비동기화 메서드로 변경하면 모니터를 획득하는데 시간을 줄일 수 있다.
// 메서드에서 사용하는 경우
public synchronized void method(){
}
// 객체 변수에 사용하는 경우
private Object obj = new Object();
public void exampleMethod(){
synchronized(obj){
}
}
5) 동적 바인딩, 동적 클래스 로딩
동적 바인딩은 프로그램 실행 중 함수가 호출 될 때 메모리 참조를 알아내는 것을 뜻한다. 이는 메서드 오버로딩이나 오버라이딩 구현 개념이다. 즉, 메서드가 같은 이름이라도 인자나 리턴값에 따라 동적으로 호출되기 때문에 호출 시 그 메서드를 참조해야한다. 이 때 메서드가 호출된 메모리 참조를 알아내는 작업을 수행하는 시간이 약간의 시간을 잡아 먹는다.
동적 클래스 로딩은 실행 중인 클래스가 다른 클래스를 참조할 때 발생한다. 이 때 해당 클래스를 직접 참조로 바꾸어 로딩을 하고 JVM에 의해 안정성 검사가 이뤄진 후 클래스 초기화가 된다. 이것도 시간을 잡아 먹기에 무분별한 클래스 인스턴스 생성은 자제해주는 것이 좋다.
6) 향상된 for문
일반적인 for문에서 사용되는 i값에 대한 인덱싱이 없기에 향상된 포문이 더 빠르다.
'코딩 테스트 > 개념 학습 및 정리' 카테고리의 다른 글
[개념 학습 및 정리] 정수론 (0) | 2021.01.14 |
---|---|
[개념 학습 및 정리] 시간 복잡도 (0) | 2021.01.14 |
[개념 학습 및 정리] 정렬 (0) | 2021.01.14 |
[개념 학습 및 정리] 완전 탐색 (0) | 2021.01.11 |