본문 바로가기

Study/개발일지

[백엔드온라인TIL] java 학습 8일차

오늘 한일

- 큰따옴표입력 

- 제네릭 객체 복습

- map 개념 복습

 

Java에서 문자열(String)은 큰따옴표로 감싸서 표현합니다.

그렇다면, 큰따옴표가 문자열 안에 포함되도록 하려면 어떻게 해야 할까요?

Hello "World"

위 문자열을 출력해야 한다면, 이 문자열을 어떻게 표현해야 할까요?

String str = "Hello "World"";

위와 같이 표현해주면 될까요?

아마도, 위와 같이 문자열을 선언을 하면 컴파일 에러가 발생할 것입니다.

그래서 이번에는 문자열 안에서 쌍따옴표를 표현하는 3가지 방법을 알아보도록 하겠습니다.

 

 

1. 이스케이프(Escape) 문자 사용하기

 코드 

 
public class StringQuotes {
 
public static void main(String[] args) {
 
 
 
String str = "Hello \"World\"";
 
 
 
System.out.println(str);
 
}
 
}

 결과 

Hello "World"

문자열 안에서 큰따옴표를 표현하기 위해서 사용하는 가장 일반적인 방법입니다.

이스케이프(escape) 문자는

백슬래시(\)와 함께 쓰이면서, 자바에서 특별한 기능을 수행하게 됩니다.

위 예제에서는 문자열 안에서 큰따옴표를 표시하기 위해서

\" 이스케이프 문자가 사용되었습니다. 

이렇게 따옴표를 백슬래시(\)와 같이 사용하면,

이 따옴표는 문자열을 열거나 닫는 용도로 사용되지 않고,

문자열 안에 포함됩니다.

 

 

 

 

2. char 사용하기

 코드 

 
public class StringQuotes {
 
public static void main(String[] args) {
 
 
 
char quotes = '"';
 
String str = "Hello " + quotes + "World" + quotes;
 
 
 
System.out.println(str);
 
}
 
}

 결과 

Hello "World"

문자열 안에서 큰따옴표를 표현하기 위해서 char를 사용하였습니다.

위 예제에서는,

큰따옴표를 char 타입으로 표현하고, 기존의 문자열에 이 char를 이어붙이는 방법을 사용하였습니다.

 

 

 

3. Unicode 사용하기

 코드 

 
public class StringQuotes {
 
public static void main(String[] args) {
 
 
 
char quotes = '\u0022';
 
String str = "Hello " + quotes + "World" + quotes;
 
 
 
System.out.println(str);
 
}
 
}

 결과 

Hello "World"

마지막 방법으로, 

쌍따옴표를 표시하는 Unicode를 찾아서 직접 입력해 주었습니다.

'\u0022'는 쌍따옴표를 나타내는 Unicode 입니다.

이렇게 직접 Unicode를 입력해주면

쌍따옴표뿐만 아니라 어떠한 특수문자라도 문자열에 포함시키고 출력할 수 있습니다.

하지만, 이 방법은 나중에 이 코드를 해석하는데 어려움이 따르는 단점이 있습니다.

나중에 이 소스 코드를 다시 본다면, 

'\u0022'가 어떤 문자를 나타내는지 따로 찾아보아야만 코드 해석이 가능하겠죠?

그래서, 가능하다면 이렇게 유니코드를 직접 쓰는 것보다는 

이스케이프 문자를 사용하거나,

char에 쌍따옴표를 직접 입력해서 사용하는 방법을 추천합니다.

 

 


 

문자열에 쌍따옴표를 표현하는 3가지 방법을 알아보았습니다.

일반적으로 이스케이프 문자를 사용하는 방법을 가장 많이 사용합니다.

상황에 맞추어 가장 적절한 방법을 찾아서 사용하세요.

 

-----------------------------------------

'제네릭(Generic) 기본적인 개념 이해하기'

 

'데이터 형식에 의존하지 않고, 하나의 값이 여러 다른 데이터 타입들을 가질 수 있도록 하는 방법'

어떤 자료 구조를 만들어서 사용하려고 할 때 String 타입도 지원하고 싶고, Integer 타입도 지원하고 싶고, 다른 타입들도 지원하고 싶은 경우가 있습니다. 그럴 때 String에 대한 클래스, Integer에 대한 클래스 등 타입에 따라 각각의 클래스들을 모두 만드는 것은 너무 비효율적입니다. 자바에서는 이러한 문제를 해결하기 위해 java 1.5부터 제네릭을 사용하게 되었는데요.

 

제네릭(Generic)은 클래스 내부에서 지정하는 것이 아닌, 외부에서 사용자에 의해 지정되는 것을 의미하며, 한마디로 특정(Specific) 타입을 미리 지정해주는 것이 아니라 필요에 의해서 지정할 수 있도록 하는 일반(Generic) 타입을 이야기합니다.

(정확하게는 지정된 것보다 타입의 경계를 지정하고, 컴파일 때 해당 타입으로 캐스팅하여 매개변수화 된 유형을 삭제하는 것)

 

사용되는 측면에서 보면 제네릭 객체는 인스턴스 별로 다르게 동작할 수 있도록 만들어졌고, 다룰 객체의 타입을 미리 명시해줌으로써 번거로운 형 변환을 줄여주게 됩니다.

제네릭을 모르면 Java API 문서를 제대로 볼 수 없다고 할 정도로 제네릭은 자바에서 중요한 기능입니다.

 

 

'제네릭(Generic)의 장점'

  • 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있습니다.
  • 클래스 외부에서 타입을 지정해주기 때문에 따로 타입을 체크하고 변환해줄 필요가 없어 관리하기가 편합니다.
  • 비슷한 기능을 지원하는 경우 코드의 재사용성을 높일 수 있습니다.

 

***

다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스는 어떤 자료를 담을지 알 수 없기 때문에 최상위 객체인 Object 타입으로 저장, 관리됩니다. 이 경우 의도치 않은 자료형이 담겨 실행 시에 오류가 발생할 수 있는데요. 해당 오류는 컴파일 시에는 알 수 없는 오류지만 제네릭(Generic) 타입을 지정하면 컴파일 시 오류를 확인할 수 있게 됩니다.

 

 


 

 

'제네릭의 사용'

// 클래스
public class ClassName <T> { ... }

// 인터페이스
public Interface InterfaceName <T> { ... }

// 제네릭 타입을 두 개 이상 사용하는 경우
public class MultiGeneric <K, V> { ... }

 

클래스나 인터페이스에 선언한 경우와 제네릭 타입을 두 개 이상 사용하는 경우입니다.

이때 T, K, V 타입은 해당 블록 {...} 안에서까지 유효합니다.

 

 

public class Box<T> {

    private T object;

    public void set(T object) {
        this.object = object;
    }
    public T get() {
        return object;
    }
}

<T>의 T를 '타입 변수'라고 합니다. 임의의 참조형 타입을 의미합니다.

 

 

T : Object       <T>
E : Element      <E>
K : Key          <K>
V : Value        <V>
N : Number       <N>

* 타입 변수(Type Variable)

기호의 종류만 다를 뿐 '임의의 참조형 타입'을 의미한다는 것은 모두 같으며, 상황에 맞게 의미 있는 문자를 선택해서 사용합니다.

(통상적으로 쓰이는 암묵적인 규칙이며, 많이 사용되는 예시일 뿐 꼭 한 글자일 필요는 없습니다.)

 

 

***

주의해야할 점은 타입 변수로 명시할 수 있는 것은 참조 타입(Reference Type)밖에 올 수 없다는 것입니다.

즉 int, double, char 같은 Primitive Type은 올 수 없습니다. 그래서 int형, double형 등의 Primitive Type의 경우 Integer, Double 같은 Wrapper Type으로 사용해야 합니다.

 

 

Test<Integer> test = new Test<Integer>();
List<String> list = new ArrayList<>();

실제 사용에서는 꺽쇠 괄호  <> 안에 있는 String을 실 타입 매개변수(Actual Type Parameter)라고 하며, 실제 List 인터페이스에 선언되어 있는 List<E>의 E를 형식 타입 매개변수(Formal Type Parameter)라고 합니다.

 

 

***

제네릭 타입은 컴파일 시 컴파일러에 의해 제거됩니다.

자바 코드에서 선언되고 사용된 제네릭 타입은 컴파일 시 컴파일러에 의해 자동으로 검사 되어 타입으로 변환됩니다. 그렇게 코드 내의 모든 제네릭 타입은 제거되어, 컴파일된 class 파일에는 어떠한 제네릭 타입도 포함되지 않게 됩니다. 제네릭이 이런 식으로 동작하는 이유는 제네릭을 사용하지 않는 코드와의 호환성을 유지하기 위해서입니다.

 

 


 

 

'제네릭의 제한'

class Box<T>{
    static T item; //에러
    static int compare(T t1, T t2) {} //에러
}

- static 멤버

 

제네릭은 객체별로 다르게 동작하기 위해서 만들어졌습니다. 때문에 모든 객체에 대해 동일하게 동작해야 하는 static 멤버에 타입 변수를 사용할 수 없습니다. 타입 변수는 인스턴스 변수로 간주됩니다. 

 

다시 이야기하면 static 변수는 인스턴스에 종속되지 않는 클래스 변수로써 모든 인스턴스가 공통된 저장 공간을 공유하게 되는 변수입니다.

static 변수에 제네릭을 사용하려면 GenericArrayList<Integer>에서는 Integer 타입으로, GenericArrayList<String>에서는 String 타입으로 사용될 수 있어야 하는데, 하나의 고유 변수가 생성되는 인스턴스에 따라 타입이 바뀐다는 개념 자체가 말이 안 되는 것입니다.

(static 변수, static 함수 등 static이 붙은 것들은 기본적으로 프로그램 실행 시 메모리에 이미 올라가 있습니다.)

 

 

public class Test<T> {
    T[] tArr1;
    T[] tArr2 = new T[10];    // error
}

- 제네릭 타입의 배열

 

제네릭 배열 타입의 참조 변수를 선언한 것은 가능하지만, 배열을 생성하는 것은 안 됩니다.

new 연산자는 컴파일 시점에서 타입 변수가 어떤 것인지 정확하게 알아야 하는데 제네릭 클래스는 컴파일하는 시점에 타입 변수가 어떤 타입이 될지 전혀 알 수가 없기 때문입니다.

 

(new 연산자는 heap 영역에 충분한 공간이 있는지 확인한 후 메모리를 확보하는 역할로, 충분한 공간이 있는지 확인하기 위해서는 타입을 알아야 하는데 컴파일 시점에서 타입 T가 무엇인지 알 수 없기 때문에 제네릭으로 배열을 생성할 수 없습니다.)

 

* instanceof 메서드도 같은 이유로 타입 변수를 사용할 수 없습니다.

 

* 제네릭 타입의 배열을 생성해야 하는 경우에는 'Reflection API'의 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나, Object 배열을 생성해서 복사한 후 'T[]'로 형 변환하는 방법 등을 사용해야 합니다.

 

 


 

 

'제네릭 메서드(Generic Method)'

public <T> T genericMethod(T object) {	
		...
}
 
[접근 제어자] <제네릭타입> [반환타입] [메소드명]([제네릭타입] [파라미터]) {
		...	
}

클래스나 인터페이스에 제네릭을 사용하는 것처럼 메서드에도 제네릭을 적용할 수 있습니다. 주로 static 유틸리티 메서드에 유용하게 쓰일 수 있는데요.

위 예시에서 볼 수 있는 것처럼 제네릭 메서드의 타입 매개변수를 선언할 때 타입 매개변수의 위치는 메서드의 접근 지시자와 반환 타입 사이가 됩니다.

 

제네릭 메서드를 정의할 때 중요한 것은 리턴 타입이 무엇인지와는 상관없이 해당 메서드가 제네릭 메서드라는 것을 컴파일러에게 알려주는 것입니다. 그러기 위해서 리턴 타입을 정의하기 전에 제네릭 타입에 대한 정의가 반드시 필요합니다.

 

또 하나 중요한 것은 제네릭 클래스가 아닌 일반 클래스 내부에서도 제네릭 메서드를 정의할 수 있다는 것인데요. 그 말은 클래스에 지정된 타입 파라미터와 제네릭 메서드에 지정된 타입 파라미터는 상관이 없다는 것입니다.

즉, 제네릭 클래스에 <T>를 사용하고, 같은 클래스 내부의 제네릭 메서드에도 <T>로 같은 이름을 가진 타입 파라미터를 사용하더라도 이 둘은 전혀 상관이 없다는 것입니다.

 

 

***

static 변수에는 제네릭을 사용할 수 없지만 static 메서드에는 제네릭을 사용할 수 있는 이유가 무엇일까요?

static 변수의 경우에는 앞에서 이야기한 것처럼 제네릭을 사용하면 여러 인스턴스에서 어떤 타입으로 공유되어야 할지 지정할 수가 없어서 사용할 수 없었습니다. static 변수는 값 자체가 공유되기 때문에 값 자체가 공유되려면 타입에 대한 정보도 있어야 하기 때문입니다.

 

반면 static 메서드의 경우 메서드의 틀만 공유된다고 생각하면 됩니다. 그리고 그 틀 안에서 지역변수처럼 타입 파라미터가 다양하게 오가는 형태로 사용될 수 있는 것입니다.

 

 

 

'제네릭 메서드(Generic Method) 예시'

public static <T extends CharSequence> void printFirstChar(T param) {
    System.out.println(param.charAt(0));
}

제네릭 메서드 선언 시 <T>만 사용해도 상관없습니다. 위 예시의 경우 charAt() 메서드를 호출하기 위해서 CharSequence의 서브타입만 가능하다는 제약을 넣은 것입니다.

printFirstChar() 제네릭 메서드를 GenericArrayList에 정의해 주었다면 호출은 아래와 같이 하면 됩니다.

 

 

GenericArrayList.<String>printFirstChar("JAN");

그런데 여기서 "JAN"을 통해 인자의 타입이 String인 것을 컴파일러가 추론할 수 있으므로 <String>은 생략 가능합니다.

대부분의 경우 타입 추론이 가능하므로 아래와 같이 타입은 생략하고 호출할 수 있습니다.

 

 

GenericArrayList.printFirstChar("JAN");

 <String> 생략

 

 


 

 

'와일드카드(Wildcards)'

public Map<String, ? super Object> getErrorMap() {
    return errorMap;
}

와일드카드는 기호 '?'를 사용합니다. 

타입 변수는 보통 단 하나의 타입만 지정하지만 와일드카드를 이용하면 하나 이상의 타입을 지정할 수 있습니다.

타입 변수의 다형성을 적용하여 어떠한 타입도 적용할 수 있게 되는데요.

 

 

<K extends T>	// T와 T의 자손 타입만 가능 (K는 들어오는 타입으로 지정 됨)
<K super T>	// T와 T의 부모(조상) 타입만 가능 (K는 들어오는 타입으로 지정 됨)
 
<? extends T>	// T와 T의 자손 타입만 가능
<? super T>	// T와 T의 부모(조상) 타입만 가능
<?>		// 모든 타입 가능. <? extends Object>랑 같은 의미

(K는 특정 타입으로 지정된다는 의미이고 ? 는 타입이 지정되지 않는다는 의미입니다.)

 

 

 

* 와일드카드가 고안된 이유

static 메서드에 제네릭을 적용한 경우, 타입 매개변수는 사용하지 못하므로 특정 타입을 지정해야 합니다. 그렇게 되면 해당 메서드는 특정 타입의 객체만을 사용할 수 있게 되어 다른 타입의 객체를 매개변수로 오게 하려면 타입 변수만 다른 똑같은 메서드를 만들어야 합니다.

 

static void method(Box<TypeA> b) {}   //   Compile error
static void method(Box<TypeB> b) {}   //   Compile error
static void method(Box<TypeC> b) {}   //   Compile error

이때 제네릭 타입이 다른 것만으로는 오버로딩이 성립되지 않기 때문에 메서드가 중복으로 정의되게 되는데요. 와일드카드는 이러한 상황에 사용하기 위해 고안되었습니다.

 

 

------------------------------------------------------------------------

 

Map

Map은 Key와 Value 한쌍으로 이루어진 자료형이다.

  • Map은 리스트나 배열처럼 순차적으로 해당 요소 값을 구하지 않고 Key를 통해 Value를 얻는다.
  • 값(Value)은 중복될 수 있지만, Key는 고유한 값(Unique)을 가져야 한다.

위와 같은 특징으로,

Map은 저장 순서를 유지할 필요가 없고, Key를 통해 Value를 얻어내기 때문에 Key는 중복을 허용하지 않는다.

만약 이미 존재하는 Key값과 동일한 Key값을 put하면 새로운 Key값으로 갱신된다.

아래의 예제를 통해서 살펴보자.


예제 1.

public class PutTheSameName {
	public static void main(String[] args) {
	    Map<String, String> map = new HashMap<String, String>();
	    map.put("김씨", "어부");
	    map.put("이씨", "광부");
	    map.put("박씨", "농부");
	    map.put("김씨", "무직");
        
        for(String key : map.keySet())
        	System.out.println(key + " : " + map.get(key));

Map객체에 위와 같이 데이터를 넣었다고 해보자.

이미 존재하는 Key 값에 동일한 Key값을 넣었을 때 어떠한 결과가 나올까?

김씨라는 Key값을 2번 넣었지만, 김씨라는 Key값이 2번 출력되지는 않는다.

Key값이 이미 존재하는 경우 Value 값을 갱신하기 때문이다.


예제 2.

앞서, Map은 순서를 유지하지 않는다고 했는데 아래와 같은 예제로 확인해볼 수 있다.

public class OrderOfKeys {
	public static void main(String[] args) {
   	    Map<String, Integer> map = new HashMap<String, Integer>();
    
	    map.put("김씨", 1);
	    map.put("이씨", 2);
	    map.put("박씨", 3);
	    map.put("최씨", 4);
 	    map.put("정씨", 5);
        
	    for(String key : map.keySet())
   	        System.out.print(key, map.get(key));

위와 같이 출력하면 map에 저장한 순서대로 값이 출력될까?

아래의 결과처럼 순서대로 출력되지 않는다.


📌 다시 말하자면, Map은

  • Key를 통해 Value를 찾는다.
  • Key는 중복될 수 없지만, Value는 중복될 수 있다.

✅ 따라서,

  • Key로 Value를 찾기 때문에 순서를 유지하지 않아도 된다.
  • Key에 해당하는 값이 이미 존재하면 값을 갱신한다.**
728x90