본문 바로가기
목차
JAVA(자바)

[자바] 공부 중

by 지각생 2022. 4. 3.
728x90

5-1. 참조 타입과 참조 변수

기본 타입 (Primitive Type)

정수타입

  1. byte
  2. char
  3. short
  4. int
  5. long

실수타입

  1. float
  2. double

논리타입

  1. boolean

 

참조타입 (Reference Type)

배열타입

열거타입

클래스

인터페이스

 

메모리 사용 영역

메소드 영역

: 클래스 별로 정적 필드, 상수, 생성자, 메소드, 코드 등을 분류해 저장

힙 영역

: 객체와 배열이 생성되는 영역

JVM 스택 영역

: 메소드가 호출되면 프레임이 추가되고, 메소드가 종료되면 프레임이 제거 됨.

 

String 객체 equals 쓰는 이유 (참조영역과 힙영역 추가 설명)

기본 타입은 스택 영역에 변수를 만들어 값을 저장한다.

참조 타입은 힙 영역에 객체를 만들어 주소를 참조한다.

 

여기서 String을 비교할때 ==이 아니라 equals 연산을 하는 이유를 알고 넘어가자.

==연산자는 기본 타입이면 변수의 값이 같은지, 다른지를 확인한다.

참조 타입이면 동일한 힙영역의 객체를 참조하는지, 다른 힙영역의 객체를 참조하는지를 확인한다.

 

String은 참조타입이다. 따라서 값이 같더라도 다른 힙 영역 내에 객체를 참조하고 있다면

== 연산자를 썼을 때 false가 나온다.

따라서 동일한 객체를 참조하건 다른 객체를 참조하건 상관없이 문자열만 비교하고 싶을때는

String 객체의 equals()메소드를 사용해야 한다.

 

5-2. 배열

배열

  1. 데이터를 연속된 공간에 나열하고 각 데이터에 인덱스 부여한 자료구조
  2. 같은 타입의 데이터만 저장할 수 있음
  3. 한 번 생성된 배열은 길이를 늘리거나 줄일 수 없음

 

배열 생성

  1. 값 목록으로 배열 생성
  2. new 연산자 이용해서 배열 생성 (길이만 선언) (int[]의 경우 0, String[]의 경우 null이 값으로 저장 됨)
  3. new 연산자 이용해서 배열 생성 
package javastudy;

public class ArrayStudy {

	public static void main(String[] args) {
		
		//값 목록으로 배열 생성 (값, 길이 생성)
		String[] string1 = {"a", "b", "c"};
		int num = string1.length; // 배열 길이
		
		//변수 선언 후  불가능. 이미 생성된 배열 길이 늘리거나 줄일 수 없음.
		String[] string2;
		string2 = {"a", "b", "c"};
		
		//new 연산자를 이용해서 배열 생성 (길이만 선언)
		int[] int1 = new int[30];
		int1[0] = 1; //배열 값 추가
		
		//변수 선언 후  가능. 다른 객체를 만들어 주소 연결 해줌. (값, 길이 생성)
		String[] string3;
		string3 = new String[] {"a", "b", "c"};
	}
}

2차원 배열

int[][] scores = new int[2][3];

 

참조 타입 배열

String[] strArray = new String[3];
strArray[0] = "java";
strArray[1] = "java";
strArray[2] = new String("java");
		
System.out.println(strArray[0] == strArray[1]); //true (같은 객체를 참조)
System.out.println(strArray[0] == strArray[2]); //false (다른 객체를 참조)
System.out.println(strArray[0].equals(strArray[2])); //true(문자열이 동일)

 

배열 복사

  1. for문을 이용해서 요소 하나 하나를 복사
  2. System.arraycopy()를 이용한 복사.
System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length);

	String[] oldStrArray = {"a", "b", "c"};
	String[] newStrArray = new String[5];
	System.arraycopy(oldStrArray, 0, newStrArray, 0, oldStrArray.length);

 

향상된 for문

  1. 배열이나 컬렉션을 좀 더 쉽게 처리
  2. 반복 실행 위해 루프 카운터 변수나 증감식 사용하지 않음.
	int[] scores = {1,2,3,4,5};
	
	for(int score : scores) {
		System.out.println(score);
	}
    
    //1
    //2
    //3
    //4
    //5

 

1. 얕은 복사(shallow copy)

  • 한 쪽에서 수정이 발생되면 다른쪽에도 영향을 끼쳐 같아지게 된다.
  • 가능한 이유는 얕은 복사가 주소값을 복사하기 때문에 주소로 값을 참조하여 값이 변경되면 해당 값을 참조하고 있는 배열들의 값이 변경된다.
  • 즉, 복사된 배열이나 원본 배열이 변경될 때, 함께 변경된다. = 연산자는 얕은 복사를 수행한다.
int[] a = new int[2];
a[0] = 2;
a[1] = 4;
int[] b = a;
b[0] = 6;
b[1] = 8;

System.out.println(a[0]+", "+a[1]);
System.out.println(b[0]+", "+b[1]);

// 결과
6, 8
6, 8
  • 위의 코드처럼 1차원 배열을 = 연산자를 사용해 얕은 복사를 진행하게 되면 복사된 배열의 값이 변경될 때, 원본 배열의 값도 변경된다.
  • 2차원 배열도 마찬가지이다. 다음 코드를 보자.
int[][] a = new int[2][2];
int[][] copy = a;

copy[0][0] = 1;

for (int[] aa : a) {
    for (int v : aa) System.out.print(v + " ");
    System.out.println();
}
System.out.println();
for (int[] aa : copy) {
    for (int v : aa) System.out.print(v + " ");
    System.out.println();
}

// 결과
1 0
0 0

1 0
0 0
  • = 연산자를 이용해 2차원 배열에서 얕은 복사가 이뤄졌다. 그렇다면 이러한 문제를 어떻게 해결할 수 있을까? 깊은 복사를 사용하자!

.

 

2. 깊은 복사(Deep copy)

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

그 중 3가지 방법은 아래와 같다.

 

1) 복사 생성자 또는 복사 팩터리를 이용하여 복사합니다
2) 직접 객체 생성하여 복사합니다 

3) Cloneable을 구현하여 clone() 재정의 

하지만 3) clone() 재정의는 final 인스턴스 또는 배열이 아닌 경우 사용을 권하지 않는다고 합니다

 

clone() 재정의 경우는 주의해서 진행해야합니다
자세한 내용은 아래 링크를 통해 참고해주세요
jackjeong.tistory.com/m/30
 

[이펙티브자바 3판] ITEM13. clone 재정의는 주의해서 진행하라

이번장의 핵심은... 새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안되며, 새로운 클래스도 이를 구현해서는 안 된다. final 클래스라면 Cloneable을 구현해도 위험이 크지 않지만, 성능

jackjeong.tistory.com

 
따라서 cloneable을 구현하여 clone()을 재정의하기보다는 복사 생성자나 복사 팩터리를 이용하여 Deep copy하는게 좋습니다

1. 복사생성자 or 복사팩터리

public class Jackcoding {

   // 복사 생성자
	public Jackcoding(Jackcoding jackcoding) {
		this.name = jackcoding.name;
		this.momey = jackcoding.momey;
	}

	// 복사 팩터리
	public static Jackcoding newInstance(Jackcoding jackcoding) {
		Jackcoding j = new Jackcoding();
		j.name = jackcoding.name;
		j.momey = jackcoding.momey;
		return j;
	}
}

 

2. 직접 객체를 생성하여 복사 

@Test
void deepCopy() {
  JackCoding jack = new JackCoding("jack", 10000);
  JackCoding jackCopy = new JackCoding();
  jackCopy.setName(jack.getName());
  jackCopy.setMoney(jack.getMoney());

  jack.changeName("coding");
  jack.spend(3000);
}

 

3. Cloneable을 구현하여 clone() 재정의
(final 인스턴스 또는 배열이 아닌 경우 추천하지 않습니다)

public class JackCoding implements Cloneable {
  
  String name;
  long money;
	
  ... 

  @Override
  protected JackCoding clone() throws CloneNotSupportedException {
    return (JackCoding) super.clone();
  }
@Test
void deepCopy() throws CloneNotSupportedException {
    JackCoding jack = new JackCoding("jack", 10000);
    JackCoding jackCopy = jack.clone();

    jack.changeName("coding");
    jack.spend(3000);
  }

이제 결과를 보면 실제값이 복사된 것을 확인할 수 있습니다

jack은 JackCoding{name='coding', money=7000}

jackCopy는 JackCoding{name='jack', money=10000}

System.out.println(jack); // JackCoding{name='coding', money=7000}
System.out.println(jackCopy); // JackCoding{name='jack', money=10000}

한번 그림을 통해 볼까요?

JackCoding jack = new JackCoding("jack", 10000);
JackCoding jackCopy = jack.clone();

실제 jack 인스턴스 참조값이 참조하고 있는 실제값을 Heap 영역에 복사합니다

그리고 jackCopy는 복사한 값의 참조값을 가집니다

 

 

jack 인스턴스의 실제값을 복사

 

 

이제 jack 인스턴스의 실제값을 바꿔볼까요?

jack.changeName("coding");
jack.spend(3000);

 

아래 그림처럼 jack의 참조값이 참조하고 있는 값을 변경하더라도 jackCopy는 관심도 없습니다

jackCopy의 참조값이 참조하는 값은 jack 인스턴스와는 관련없는 독립된 값이기 때문입니다

 

 

 

 

이렇게 shallow copy와 deep copy에 대해 알아보았습니다

감사합니다

 

 

 

 

13-1 컬렉션 프레임워크(Collection FrameWork)

  1. 자료구조를 사용해서 객체들을 효울적으로 관리할 수 있도록 인터페이스와 구현 클래스를 java.util 패키지에서 제공함.
  2. 프레임워크 : 사용방법을 정해놓은 라이브러리
  3. 주요 인터페이스로 List, Set, Map이 있음.

List 컬렉션

  1. 객체를 인덱스로 관리.
  2. 객체 저장시 자동 인덱스 부여 & 저장용량 자동 증가.
  3. 객체 번지 참조. (null도 저장 가능)

객체 추가

boolean add(E e)

:주어진 객체를 맨 끝에 추가합니다.

 

void add(int index, E element)

:주어진 인덱스에 객체를 추가합니다.

 

E set(int index, E element)

:주어진 인덱스에 저장된 객체를 주어진 객체로 바꿉니다.

객체 검색

boolean contains(Object o)

:주어진 객체가 저장되어 있는지 조사합니다.

 

E get(int index)

:주어진 인덱스에 저장된 객체를 리턴합니다.

 

boolean isEmpty()

:컬렉션이 비어 있는지 조사합니다.

 

int size()

:저장되어 있는 전체 객체 수를 리턴합니다.

 

객체 삭제

void clear()

:저장된 모든 객체를 삭제합니다.

 

E remove(int index)

:주어진 인덱스에 저장된 객체를 삭제합니다.

 

boolean remove(Object o)

:주어진 객체를 삭제합니다.

 

List 인터페이스의 구현 클래스

ArrayList

  1. 객체 추가시 0번 인덱스부터 차례로 0,1,2,3,4....번에 저장.
  2. 객체 제거시 바로 뒤 인덱스부터 마지막 인덱스까지 모두 앞으로 1씩 당겨짐.

Vector

  1. 동기화된 메소드로 구성되어 멀티 스레드가 동시에 Vector의 메소드들 실행할 수 없고, 하나의 스레드가 메소드 실행 완료해야만 다른 스레드가 메소드 실행할 수 있음.
  2. 멀티 스레드 환경에서 안전하게 객체 추가 및 삭제할 수 있음.
  3. 저장할 객체 타입을 타입 파라미터로 표기하고 기본 생성자 호출하여 생성.
List<E> list = new Vector<E>();
List<E> list = new Vector<>();

 

 

LinkedList

  1. ArrayList와 사용 방법은 같으나 내부 구조가 다름.
  2. 인접 참조를 링크하여 체인처럼 객체를 관리
  3. 특정 인덱스 객체 제거하거나 삽입하면 앞뒤 링크만 변경되고 나머지 링크는 변경되지 않음.
  4. 저장할 객체 타입을 타입 파라미터로 표기하고 기본 생성자 호출하여 생성.

 

List<E> list = new LinkedList<E>();
List<E> list = new LinkedList<>();

 

 

 (java.util 패키지에 내장되어있는 LinkedList가 있다. 그렇지만 원리를 위해 직접 구현해보면 다음과 같다.)

package data_structure;

class Node {
    int data;
    Node next;
    Node(int data){
        this.data = data;
    }
}

//LinkedList의 틀을 추상화할 main class
public class SinglyLinkedList {
    Node head;

    public void add(int data){
        if(head == null){
            head = new Node(data);
            return;
        }

        while(head.next != null){
            head = head.next;
        }
        head.next = new Node(data);
    }

    public void remove(int data){
        // 배열에 아무것도 없을 경우
        if(head == null) return;
        // data가 맨 앞에 있을 경우.
        if(head.data == data) {
            head = head.next;
            return;
        }

        Node current = head;
        while(current.next != null) {
            if(current.next.data == data){
                current.next = current.next.next;
                return;
            }
            current = current.next;
        }
    }

    public static void printSinglyLinkedList(SinglyLinkedList list){
        Node node = list.head;
        System.out.println(node.next.data);
//        while(node.next != null){
//            System.out.print(node.data + " ");
//            node = node.next;
//        }
//
//        if(node != null) System.out.print(node.data);

    }

    public static void main(String[] args) {
        SinglyLinkedList list = new SinglyLinkedList();

        for(int i=1; i<=7; i++){
            list.add(i);
        }

        printSinglyLinkedList(list);

    }
}

Set 컬렉션

  1. 저장 순서 없음.
  2. 객체 중복 불가.
  3. 하나의 null만 저장 가능.

객체추가

boolean add(E e)

:주어진 객체를 저장. 객체가 저장되면 true, 중복되면 false리턴

 

package javastudy;

import java.util.HashSet;
import java.util.Set;

public class SetStudy {
	public static void main(String[] args) {
		Set<String> set= new HashSet<>();//HashSet, LinkedHashSet, TreeSet 쓰면 됨.
		set.add("a");
		set.add("b");
		set.remove("홍길동");
	}
}

 

객체 검색

boolean contains(Object o)

:주어진 객체가 저장되어 있는지 조사함.

 

boolean isEmpty()

:컬렉션이 비어 있는지 조사함.

 

int size()

:저장되어 있는 전체 객체수 리턴.

 

Iterator<E> iterator()

:저장된 객체를 한번씩 가져오는 반복자를 리턴.

package javastudy;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class IteratorStudy {
	public static void main(String[] args) {
		Set<String> set = new HashSet<>();
		set.add("a");
		set.add("b");
		set.add("c");
		Iterator<String> iterator = set.iterator();
		
		while (iterator.hasNext()) {
			String str = iterator.next();
			if (str.equals("a")) {
				iterator.remove();
			}
			System.out.println(set);
		}
	}
}

//결과
//[b, c]
//[b, c]
//[b, c]

//고찰
//1. 객체가 3개이므로 3번 반복되어 [b, c]가 3번 출력됨.

//2. [b, c]가 배열처럼 보이길래
//   System.out.println(set[0]);의 출력을 시도해봤지만
//   Set은 순서가 없기 때문에 에러가 출력 됨.

//3. Set은 순서가 없는데 Iterator를 쓰니 반복하여 쓸 수 있음.

//4. for문으로는 Set을 반복할 수 없는건가? 하는 의문이 든다.

//5.  10,11,12행과 13행의 순서는 중요하다. 
//    10,11,12행을 13행보다 먼저 써야 15행에 hasNext()가 true를 반환한다. 뒤에 쓰면 false반환.

6. iterator.next()란 표현을 보면 3개 객체 중 next 때문에 2회만 반복될까 우려도 됐지만 3회 모두 반복 됨.

 

 

객체 삭제

void clear()

:저장된 모든 객체를 삭제.

 

boolean remove(Object o)

:주어진 객체를 삭제.

 

Set 인터페이스의 구현 클래스

HashSet

  1. 저장 순서 없음.
  2. 객체 중복 불가.
  3. Set 인터페이스의 구현 클래스

LinkedHashSet

  1. 저장 순서 있음.
  2. 객체 중복 불가.
  3. Set 인터페이스의 구현 클래스

 

 

TreeSet

  1. 자동 정렬 있음.
  2. 객체 중복 불가.
  3. Set 인터페이스의 구현 클래스

Map 컬렉션

키와 값으로 구성된 Map.Entry 객체 저장하는 구조 가짐.

키는 중복 저장될 수 없으나 값은 중복 저장될 수 있음. (기존 저장된 키와 동일한 키로 값을 저장하면 기존 값 없어지고 새로운 값으로 대체)

객체 추가

V put(K key, V value)

: 주어진 키로 값을 저장. 새로운 키일 경우 null을 리턴하고 동일한 키가 있을 경우 값을 대체하고 이전 값을 리턴.

 

객체 검색

boolean containsKey(Object key)

: 주어진 키가 있는지 여부를 확인.

 

boolean containsValue(Object value)

: 주어진 값이 있는지여부를 확인

 

Set<Map.Entry<K,V>> entrySet()

: 키와 값의 쌍으로 구성된 모든 Map.Entry 객체를 Set에 담아서 리턴합니다.

 

V get(Object key)

: 주어진 키가 있는 값을 리턴합니다.

 

boolean isEmpty()

: 컬렉션이 비어 있는지 여부를 확인합니다.

 

Set<K> keySet()

: 모든 키를 Set 객체에 담아서 리턴합니다.

 

int size()

: 저장된 모든 값을 Collection에 담아서 리턴합니다.

 

Collection<V> values()

: 저장된 모든 값을 Collection에 담아서 리턴합니다.

 

package javastudy;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

public class MapEntryIterator {
	public static void main(String[] args) {
		
		//map 객체 추가 및 삭제
		Map<String, Integer> map = new HashMap<>();
		map.put("홍길동", 30);
		int score = map.get("홍길동");
		map.remove("홍길동");
		
		//KeySet() 메소드로 모든 키를 Set 컬렉션으로 얻은 뒤 반복자를 통해 키 하나씩 얻고 get()메소드를 통해 값 없음.
		Set<String> keySet = map.keySet();
		Iterator<String> keyIterator = keySet.iterator();
		while(keyIterator.hasNext()) {
			String key = keyIterator.next();
			Integer value = map.get(key);
		}
		
		//entrySet() 메소드로 모든 Map.Entry를 Set 컬렉션으로 얻은 뒤 반복자를 통해 Map.Entry() 하나씩 얻고 getKey()와 getValue() 메소드 이용해 키와 값 얻음.
		Set<Map.Entry<String,Integer>> entrySet = map.entrySet();
		Iterator<Map.Entry<String, Integer>> entryIterator = entrySet.iterator();
		while(entryIterator.hasNext()) {
			Map.Entry<String, Integer> entry = entryIterator.next();
			String key = entry.getKey();
			Integer value = entry.getValue();
		}
		

	}
}

객체 삭제

void clear()

: 모든 Map.Entry(키와 값)을 삭제합니다.

 

V remove(Object key)

: 주어진 키와 일치하는 Map.Entry를 삭제하고 값을 리턴합니다.

 

Map 인터페이스의 구현 클래스

HashMap

  1. 대표적 Map 컬렉션
  2. HashMap의 키로 사용할 객체는 hashCode()와 equals() 메소드 재정의하여 동등 객체가 될 조건 정해야 함.

(hashCode() 리턴값 같고 equals() 메소드가 true 리턴해야 함)

 

Hashtable

  1. HashMap과 동일한 내부 구조.
  2. 동기화된 메소드로 구성되어 멀티 스레드가 동시에 Hashtable 메소드 실행할 수 없으며, 하나의 스레드가 실행을 완료해야만 다른 스레드 실행할 수 있음.
  3. 키로 사용할 객체를 hashCode()와 equals() 메소드 재정의하여 동등 객체 될 조건 정해야 함.

 

LinkedHashMap

Properties

TreeMap

 

13-2. LIFO와 FIFO 컬렉션

후입선출 (LIFO : List In First Out)

:나중에 넣은 객체가 먼저 빠져나가는 자료구조. 컬렉션 프레임 워크에는 LIFO 자료구조 제공하는 Stack 클래스 제공

 

선입선출 (FIFO : First In First Out)

:먼저 넣은 객체가 먼저 빠져나가는 자료구조. 컬렉션 프레임 워크에는 FIFO 자료구조 제공하는 Queue 인터페이스 제공

 

Stack

1. LIFO 자료구조 구현한 클래스

리턴 타입 메소드 설명
E push(E item) 주어진 객체를 스택에 넣습니다.
E peek() 스택의 맨 위 객체를 가져옴.
객체를 스택에서 제거하지 않음.
E pop() 스택의 맨 위 객체를 가져옴.
객체를 스택에서 제거.

 

2. Stack 객체 생성하려면 저장할 객체 타입을 E 타입 파라미터 자리에 표기하고 기본 생성자를 호출

package javastudy;

import java.util.Stack;

public class StackStudy {
	public static void main(String[] args) {
		
		Stack<String> stack1 = new Stack<String>();
		Stack<Integer> stack2 = new Stack<>();

	}
}

 

 

3. 예제

package javastudy;

import java.util.Stack;

public class StackExample1 {
	public static void main(String[] args) {
		Stack<String> stack = new Stack<>();
		
		stack.push("500원");
		stack.push("50원");
		stack.push("100원");
		
		while(!stack.isEmpty()) {
			String coin = stack.pop();
			System.out.println("금액:"+coin);
		}
	}
}
//출력
//금액:100원
//금액:50원
//금액:500원

//고찰
//13행에 isEmpty()메소드를 쓸 수 있구나!
//이외에도 add, get ,iterator 등 컬렉션 메서드들 사용 가능하네

 

 

Queue

1. FIFO 자료구조에서 사용되는 메소드 정의

리턴 타입 메소드 설명
boolean offer(E e) 주어진 객체를 넣습니다.
E peek() 객체 하나를 가져옵니다.
객체를 큐에서 제거하지 않습니다.
E poll() 객체 하나를 가져옵니다.
객체를 큐에서 제거합니다.

 

2. 구현 클래스로 LinkedList 클래스가 있음.

package javastudy;

import java.util.LinkedList;
import java.util.Queue;

public class QueueStudy {
	public static void main(String[] args) {

		Queue<Integer> queue1 = new LinkedList<Integer>();
		Queue<String> queue2 = new LinkedList<>();
		Queue<String> queue3 = new Queue<>();//Cannot instantiate the type Queue
	}
}

//고찰
//11행 보면 스택과 다르게 에러가 뜸.
//9행 10행처럼 LinkedList로 구현하자.

 

3. 예제

package javastudy;

import java.util.LinkedList;
import java.util.Queue;

public class QueueExample {

	public static void main(String[] args) {

		Queue<String> que = new LinkedList<>();
		
		que.offer("홍길동");
		que.offer("고길동");
		que.offer("길동");
		
		while(!que.isEmpty()) {
		System.out.println(que.poll());	
		}
	}
}

//결과
//홍길동
//고길동
//길동

 

14-1.입출력 스트림

스트림

  1. 자바에서 데이터는 스트림을 통해 입출력됨.
  2. 프로그램이 데이터의 출발지인지 도착지인지의 여부에 따라 사용하는 스트림의 종류가 결정.

바이트 기반 스트림

문자 기반 스트림

 

제네릭

 

정적언어(C, C++, C#, Java)을 다뤄보신 분이라면 제네릭(Generic)에 대해 잘 알지는 못하더라도 한 번쯤은 들어봤을 것이다. 특히 자료구조 같이 구조체를 직접 만들어 사용할 때 많이 쓰이기도 하고 매우 유용하기도 하다.

 

잠깐 그럼 제네릭(Generic)이란 무엇인지에 대해 알고 가보도록 하자.

 

제네릭(Generic)은 직역하자면 '일반적인'이라는 뜻이다. 음.. 한 번에 이해가 가진 않는다. 조금 더 부연설명을 하자면 '데이터 형식에 의존하지 않고, 하나의 값이 여러 다른 데이터 타입들을 가질 수 있도록 하는 방법'이다.

 

우리가 흔히 쓰는 ArrayList, LinkedList 등을 생성할 때 어떻게 쓰는가?

객체<타입> 객체명 = new 객체<타입>(); 이렇게 쓰지 않는가? 즉, 아래와 같이 여러 생성방식이 있다.

 

 
ArrayList<Integer> list1 = new ArrayList<Integer>();
 
ArrayList<String> list2 = new ArrayList<Integer>();
 
 
 
LinkedList<Double> list3 = new LinkedList<Double>():
 
LinkedList<Character> list4 = new LinkedList<Character>();

 

이렇게 <> 괄호 안에 들어가는 타입을 지정해준다.

 

 

생각해보자. 만약에 우리가 어떤 자료구조를 만들어 배포하려고 한다. 그런데 String 타입도 지원하고싶고 Integer타입도 지원하고 싶고 많은 타입을 지원하고 싶다. 그러면 String에 대한 클래스, Integer에 대한 클래스 등 하나하나 타입에 따라 만들 것인가? 그건 너무 비효율적이다. 이러한 문제를 해결하기 위해 우리는 제네릭이라는 것을 사용한다.

 

 

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

 

(정확히 말하자면 지정된다는 것 보다는 타입의 경계를 지정하고, 컴파일 때 해당 타입으로 캐스팅하여 매개변수화 된 유형을 삭제하는 것이다. 이 것을 여기서 모두 설명하기에는 너무 길어지므로 일단 지정된다 정도로 알고가도 이해하는데 큰 문제는 없을 것이다.)

 

 

 

 

 


 

 

 

Generic(제네릭)의 장점

 

 

 

1. 제네릭을 사용하면 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있다.

2. 클래스 외부에서 타입을 지정해주기 때문에 따로 타입을 체크하고 변환해줄 필요가 없다. 즉, 관리하기가 편하다.

3. 비슷한 기능을 지원하는 경우 코드의 재사용성이 높아진다.

 

 









Generic(제네릭) 사용방법



 

 

보통 제네릭은 아래 표의 타입들이 많이 쓰인다. 

 

타입 설명
<T> Type
<E> Element
<K> Key
<V> Value
<N> Number

 

 

물론 반드시 한 글자일 필요는 없다. 또한 설명과 반드시 일치해야 할 필요도 없다. 예로들어 <Ele>라고 해도 전혀 무방하다. 다만 대중적으로 통하는 통상적인 선언이 가장 편하기 때문에 위와같은 암묵적(?)인 규칙이 있을 뿐이다.

 

 

그럼 각 상황별 선언 및 생성 방법을 알아보자.

 

 

 

 

1. 클래스 및 인터페이스 선언

 

 

 
public class ClassName <T> { ... }
 
public Interface InterfaceName <T> { ... }

 

기본적으로 제네릭 타입의 클래스나 인터페이스의 경우 위와같이 선언한다.

T 타입은 해당 블럭 { ... } 안에서까지 유효하다.

 

또한 여기서 더 나아가 제네릭 타입을 두 개로 둘 수도 있다. (대표적으로 타입 인자로 두 개 받는 대표적인 컬렉션인 HashMap을 생각해보자.)

 

 
public class ClassName <T, K> { ... }
 
public Interface InterfaceName <T, K> { ... }
 
 
 
// HashMap의 경우 아래와 같이 선언되어있을 것이다.
 
public class HashMap <K, V> { ... }

 

이렇듯 데이터 타입을 외부로부터 지정할 수 있도록 할 수 있다.

 

그럼 이렇게 생성된 제네릭 클래스를 사용하고 싶을 것이다. 즉, 객체를 생성해야 하는데 이 때 구체적인 타입을 명시를 해주어야 하는 것이다.

 
public class ClassName <T, K> { ... }
 
 
 
public class Main {
 
public static void main(String[] args) {
 
ClassName<String, Integer> a = new ClassName<String, Integer>();
 
}
 
}

 

위 예시대로라면 T는 String이 되고, K는 Integer가 된다.

 

이 때 주의해야 할 점은 타입 파라미터로 명시할 수 있는 것은 참조 타입(Reference Type)밖에 올 수 없다. 즉, int, double, char 같은 primitive type은 올 수 없다는 것이다. 그래서 int형 double형 등 primitive Type의 경우 Integer, Double 같은 Wrapper Type으로 쓰는 이유가 바로 위와같은 이유다.

 

또한 바꿔 말하면 참조 타입이 올 수 있다는 것은 사용자가 정의한 클래스도 타입으로 올 수 있다는 것이다.

 
public class ClassName <T> { ... }
 
 
 
public class Student { ... }
 
 
 
public class Main {
 
public static void main(String[] args) {
 
ClassName<Student> a = new ClassName<Student>();
 
}
 
}

 

 

 

 

 

2. 제네릭 클래스

 

그러면 클래스 및 인터페이스를 제네릭으로 받는 방법을 알아봤으니 본격적으로 활용해보자.

 

 

[제네릭 클래스]

 
// 제네릭 클래스
 
class ClassName<E> {
 
 
 
private E element; // 제네릭 타입 변수
 
 
 
void set(E element) { // 제네릭 파라미터 메소드
 
this.element = element;
 
}
 
 
 
E get() { // 제네릭 타입 반환 메소드
 
return element;
 
}
 
 
 
}
 
 
 
class Main {
 
public static void main(String[] args) {
 
 
 
ClassName<String> a = new ClassName<String>();
 
ClassName<Integer> b = new ClassName<Integer>();
 
 
 
a.set("10");
 
b.set(10);
 
 
 
System.out.println("a data : " + a.get());
 
// 반환된 변수의 타입 출력
 
System.out.println("a E Type : " + a.get().getClass().getName());
 
 
 
System.out.println();
 
System.out.println("b data : " + b.get());
 
// 반환된 변수의 타입 출력
 
System.out.println("b E Type : " + b.get().getClass().getName());
 
 
 
}
 
}

 

 

보면 ClassName이란 객체를 생성할 때 <> 안에 타입 파라미터(Type parameter)를 지정한다.

 

그러면 a객체의 ClassName의 E 제네릭 타입은 String으로 모두 변환된다.

반대로 b객체의 ClassName의 E 제네릭 타입은 Integer으로 모두 변환된다.

 

실제로 위 코드를 실행시키면 다음과 같이 출력된다.

 

 

 

 

 

 

 

 

 

 

만약 제네릭을 두 개 쓰고 싶다면 이렇게 할 수도 있다.

 

 
// 제네릭 클래스
 
class ClassName<K, V> {
 
private K first; // K 타입(제네릭)
 
private V second; // V 타입(제네릭)
 
 
 
void set(K first, V second) {
 
this.first = first;
 
this.second = second;
 
}
 
 
 
K getFirst() {
 
return first;
 
}
 
 
 
V getSecond() {
 
return second;
 
}
 
}
 
 
 
// 메인 클래스
 
class Main {
 
public static void main(String[] args) {
 
 
 
ClassName<String, Integer> a = new ClassName<String, Integer>();
 
 
 
a.set("10", 10);
 
 
 
 
 
System.out.println(" fisrt data : " + a.getFirst());
 
// 반환된 변수의 타입 출력
 
System.out.println(" K Type : " + a.getFirst().getClass().getName());
 
 
 
System.out.println(" second data : " + a.getSecond());
 
// 반환된 변수의 타입 출력
 
System.out.println(" V Type : " + a.getSecond().getClass().getName());
 
}
 
}

 

 

결과는 다음과 같다.

 

이렇게 외부 클래스에서 제네릭 클래스를 생성할 때 <> 괄호 안에 타입을 파라미터로 보내 제네릭 타입을 지정해주는 것. 이 것이 바로 제네릭 프로그래밍이다.

 

 

 

 

 

3. 제네릭 메소드

 

 

 

위 과정까지는 클래스 이름 옆에 예로들어 <E>라는 제네릭타입을 붙여 해당 클래스 내에서 사용할 수 있는 E 타입으로 일반화를 했다.

그러나 그 외에 별도로 메소드에 한정한 제네릭도 사용할 수 있다.

 

일반적으로 선언 방법은 다음과 같다. 

 

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

 

클래스와는 다르게 반환타입 이전에 <> 제네릭 타입을 선언한다.

위에서 다룬 제네릭 클래스에서 활용해보도록 하자.

 

 

 

[제네릭 클래스]

 
// 제네릭 클래스
 
class ClassName<E> {
 
 
 
private E element; // 제네릭 타입 변수
 
 
 
void set(E element) { // 제네릭 파라미터 메소드
 
this.element = element;
 
}
 
 
 
E get() { // 제네릭 타입 반환 메소드
 
return element;
 
}
 
 
 
<T> T genericMethod(T o) { // 제네릭 메소드
 
return o;
 
}
 
 
 
 
 
}
 
 
 
public class Main {
 
public static void main(String[] args) {
 
 
 
ClassName<String> a = new ClassName<String>();
 
ClassName<Integer> b = new ClassName<Integer>();
 
 
 
a.set("10");
 
b.set(10);
 
 
 
System.out.println("a data : " + a.get());
 
// 반환된 변수의 타입 출력
 
System.out.println("a E Type : " + a.get().getClass().getName());
 
 
 
System.out.println();
 
System.out.println("b data : " + b.get());
 
// 반환된 변수의 타입 출력
 
System.out.println("b E Type : " + b.get().getClass().getName());
 
System.out.println();
 
 
 
// 제네릭 메소드 Integer
 
System.out.println("<T> returnType : " + a.genericMethod(3).getClass().getName());
 
 
 
// 제네릭 메소드 String
 
System.out.println("<T> returnType : " + a.genericMethod("ABCD").getClass().getName());
 
 
 
// 제네릭 메소드 ClassName b
 
System.out.println("<T> returnType : " + a.genericMethod(b).getClass().getName());
 
}
 
}

 

 

보면 ClassName이란 객체를 생성할 때 <> 안에 타입 파라미터(Type parameter)를 지정한다.

 

그러면 a객체의 ClassName의 E 제네릭 타입은 String으로 모두 변환된다.

반대로 b객체의 ClassName의 E 제네릭 타입은 Integer으로 모두 변환된다.

genericMethod()는 파라미터 타입에 따라 T 타입이 결정된다.

 

실제로 위 코드를 실행시키면 다음과 같이 출력된다.

 

 

 

즉, 클래스에서 지정한 제네릭유형과 별도로 메소드에서 독립적으로 제네릭 유형을 선언하여 쓸 수 있다.

 

 

그럼 위와같은 방식이 왜 필요한가? 바로 '정적 메소드로 선언할 때 필요'하기 때문이다.

생각해보자. 앞서 제네릭은 유형을 외부에서 지정해준다고 했다. 즉 해당 클래스 객체가 인스턴스화 했을 때, 쉽게 말해 new 생성자로 클래스 객체를 생성하고 <> 괄호 사이에 파라미터로 넘겨준 타입으로 지정이 된다는 뜻이다.

 

하지만 static 은 무엇인가? 정적이라는 뜻이다. static 변수, static 함수 등 static이 붙은 것들은 기본적으로 프로그램 실행시 메모리에 이미 올라가있다.

 

이 말은 객체 생성을 통해 접근할 필요 없이 이미 메모리에 올라가 있기 때문에 클래스 이름을 통해 바로 쓸 수 있다는 것이다.

 

 

근데, 거꾸로 생각해보자면 static 메소드는 객체가 생성되기 전에 이미 메모리에 올라가는데 타입을 어디서 얻어올 수 있을까? 

 

 
class ClassName<E> {
 
 
 
/*
 
* 클래스와 같은 E 타입이더라도
 
* static 메소드는 객체가 생성되기 이전 시점에
 
* 메모리에 먼저 올라가기 때문에
 
* E 유형을 클래스로부터 얻어올 방법이 없다.
 
*/
 
static E genericMethod(E o) { // error!
 
return o;
 
}
 
 
 
}
 
 
 
class Main {
 
 
 
public static void main(String[] args) {
 
 
 
// ClassName 객체가 생성되기 전에 접근할 수 있으나 유형을 지정할 방법이 없어 에러남
 
ClassName.getnerMethod(3);
 
 
 
}
 
}

 

위 내용을 보면 이해가 갈 것이다.

 

그렇기 때문에 제네릭이 사용되는 메소드를 정적메소드로 두고 싶은 경우 제네릭 클래스와 별도로 독립적인 제네릭이 사용되어야 한다는 것이다.

 

 

 

 

 
// 제네릭 클래스
 
class ClassName<E> {
 
 
 
private E element; // 제네릭 타입 변수
 
 
 
void set(E element) { // 제네릭 파라미터 메소드
 
this.element = element;
 
}
 
 
 
E get() { // 제네릭 타입 반환 메소드
 
return element;
 
}
 
 
 
// 아래 메소드의 E타입은 제네릭 클래스의 E타입과 다른 독립적인 타입이다.
 
static <E> E genericMethod1(E o) { // 제네릭 메소드
 
return o;
 
}
 
 
 
static <T> T genericMethod2(T o) { // 제네릭 메소드
 
return o;
 
}
 
 
 
}
 
 
 
public class Main {
 
public static void main(String[] args) {
 
 
 
ClassName<String> a = new ClassName<String>();
 
ClassName<Integer> b = new ClassName<Integer>();
 
 
 
a.set("10");
 
b.set(10);
 
 
 
System.out.println("a data : " + a.get());
 
// 반환된 변수의 타입 출력
 
System.out.println("a E Type : " + a.get().getClass().getName());
 
 
 
System.out.println();
 
System.out.println("b data : " + b.get());
 
// 반환된 변수의 타입 출력
 
System.out.println("b E Type : " + b.get().getClass().getName());
 
System.out.println();
 
 
 
// 제네릭 메소드1 Integer
 
System.out.println("<E> returnType : " + ClassName.genericMethod1(3).getClass().getName());
 
 
 
// 제네릭 메소드1 String
 
System.out.println("<E> returnType : " + ClassName.genericMethod1("ABCD").getClass().getName());
 
 
 
// 제네릭 메소드2 ClassName a
 
System.out.println("<T> returnType : " + ClassName.genericMethod1(a).getClass().getName());
 
 
 
// 제네릭 메소드2 Double
 
System.out.println("<T> returnType : " + ClassName.genericMethod1(3.0).getClass().getName());
 
}
 
}

 

 

결과는 다음과 같다.

 

 

보다시피 제네릭 메소드는 제네릭 클래스 타입과 별도로 지정된다는 것을 볼 수 있다.

<> 괄호 안에 타입을 파라미터로 보내 제네릭 타입을 지정해주는 것. 이 것이 바로 제네릭 프로그래밍이다.

 

 

그런데 이런 의문이 들 수 있다. "아니 그러면 특정 범위만 허용하고 나머지 타입은 제한 할 수 없나요?"라는 얘기가 나오기 마련이다. 당연히 가능하다. 다음 파트로 넘어가보자.





 

 





제한된 Generic(제네릭)과 와일드 카드



 

 

지금까지는 제네릭의 가장 일반적인 예시들을 보여주었다. 예로들어 타입을 T라고 하고 외부클래스에서 Integer을 파라미터로 보내면 T 는 Integer가 되고, String을 보내면 T는 String이 된다. 만약 당신이 Student 라는 클래스를 만들었을 때 T 파라미터를 Student로 보내면 T는 Student가 된다. 즉, 제네릭은 참조 타입 모두 될 수 있다.

 

근데, 만약 특정 범위 내로 좁혀서 제한하고 싶다면 어떻게 해야할까? 

 

 

이 때 필요한 것이 바로 extends 와 super, 그리고 ?(물음표)다. ? 는 와일드 카드라고 해서 쉽게 말해 '알 수 없는 타입'이라는 의미다.

 

일단 먼저 예시를 보면서 말해보자면 이용할 때 크게 세 가지 방식이 있다. 바로 super 키워드와 extends 키워드, 마지막으로 ? 하나만 오는 경우다. 코드로 보자면 다음과 같다. 

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

 

보통 이해하기 쉽게 다음과 같이 부른다.

 extends T : 상한 경계

? super T : 하한 경계

 

<?> : 와일드 카드(Wild card)

 

 

 

이 때 주의해야 할 게 있다. K extends T와 ? extends T는 비슷한 구조지만 차이점이 있다.

'유형 경계를 지정'하는 것은 같으나 경계가 지정되고 K는 특정 타입으로 지정이 되지만, ?는 타입이 지정되지 않는다는 의미다.

 

가장 쉬운 예시로 다음과 같은 예시가 있다.

 

 
/*
 
* Number와 이를 상속하는 Integer, Short, Double, Long 등의
 
* 타입이 지정될 수 있으며, 객체 혹은 메소드를 호출 할 경우 K는
 
* 지정된 타입으로 변환이 된다.
 
*/
 
<K extends Number>
 
 
 
 
 
/*
 
* Number와 이를 상속하는 Integer, Short, Double, Long 등의
 
* 타입이 지정될 수 있으며, 객체 혹은 메소드를 호출 할 경우 지정 되는 타입이 없어
 
* 타입 참조를 할 수는 없다.
 
*/
 
<? extends T> // T와 T의 자손 타입만 가능

 

 

위와 같은 차이가 있다. 그렇기 때문에 특정 타입의 데이터를 조작하고자 할 경우에는 K 같이 특정 제네릭 인수로 지정을 해주어야 한다. 

 

 

 

 

 

일단 위 설명은 잠깐 뒤로하고 extends와 super부터 한 번 하나하나 예를 들어보자.

 

 

다음과 같이 서로 다른 클래스들이 상속관계를 갖고 있다고 가정해보자.

 

 

 

 

 

1. <K extends T>, <? extends T>

이 것은 T 타입을 포함한 자식(자손) 타입만 가능하다는 의미다. 즉, 다음과 같은 경우들이 있다.

 

 
<T extends B> // B와 C타입만 올 수 있음
 
<T extends E> // E타입만 올 수 있음
 
<T extends A> // A, B, C, D, E 타입이 올 수 있음
 
 
 
<? extends B> // B와 C타입만 올 수 있음
 
<? extends E> // E타입만 올 수 있음
 
<? extends A> // A, B, C, D, E 타입이 올 수 있음

 

주석에 썼듯이 보면 알겠지만, 상한 한계. 즉 extends 뒤에 오는 타입이 최상위 타입으로 한계가 정해지는 것이다.

 

대표적인 예로는 제네릭 클래스에서 수를 표현하는 클래스만 받고 싶은 경우가 있다. 대표적인 Integer, Long, Byte, Double, Float, Short 같은 래퍼 클래스들은 Number 클래스를 상속 받는다.

 

즉,  Integer, Long, Byte, Double, Float, Short 같은 수를 표현하는 래퍼 클래스만으로 제한하고 싶은 경우 다음과 같이 쓸 수 있다.

 

public class ClassName <K extends Number> { ... }

 

이렇게 특정 타입 및 그 하위 타입만 제한 하고 싶을 경우 쓰면 된다. 좀 더 구체적으로 예로 들자면, 다음과 같다. Integer는 Number 클래스를 상속받는 클래스라 가능하지만, String은 Number클래스와는 완전 별개의 클래스이기 때문에 에러(Bound mismatch)를 띄운다.

 
public class ClassName <K extends Number> {
 
...
 
}
 
 
 
public class Main {
 
public static void main(String[] args) {
 
 
 
ClassName<Double> a1 = new ClassName<Double>(); // OK!
 
 
 
ClassName<String> a2 = new ClassName<String>(); // error!
 
}
 
}

 

 

 

 

 

2. <K super T>, <? super T>

이 것은 T 타입의 부모(조상) 타입만 가능하다는 의미다. 즉, 다음과 같은 경우들이 있다.

 

 
<K super B> // B와 A타입만 올 수 있음
 
<K super E> // E, D, A타입만 올 수 있음
 
<K super A> // A타입만 올 수 있음
 
 
 
<? super B> // B와 A타입만 올 수 있음
 
<? super E> // E, D, A타입만 올 수 있음
 
<? super A> // A타입만 올 수 있음

 

주석에 썼듯이 보면 알겠지만, 하한 한계. 즉 super 뒤에 오는 타입이 최하위 타입으로 한계가 정해지는 것이다.

 

 

대표적으로는 해당 객체가 업캐스팅(Up Casting)이 될 필요가 있을 때 사용한다.

예로들어 '과일'이라는 클래스가 있고 이 클래스를 각각 상속받는 '사과'클래스와 '딸기'클래스가 있다고 가정해보자.

 

이 때 각각의 사과와 딸기는 종류가 다르지만, 둘 다 '과일'로 보고 자료를 조작해야 할 수도 있다. (예로들면 과일 목록을 뽑는다거나 등등..) 그럴 때 '사과'를 '과일'로 캐스팅 해야 하는데, 과일이 상위 타입이므로 업캐스팅을 해야한다. 이럴 때 쓸 수 있는 것이 바로 super라는 것이다.

 

조금 더 현실성 있는 예제라면 제네릭 타입에 대한 객체비교가 있다.

 

public class ClassName <E extends Comparable<? super E> { ... }

 

이런 문구를 한 번쯤 보셨을 수 있다. 특히 PriorityQueue(우선순위 큐), TreeSet, TreeMap 같이 값을 정렬하는 클래스 만약 여러분이 특정 제네릭에 대한 자기 참조 비교를 하고싶을 경우 대부분 공통적으로 위와 같은 형식을 취한다.

 

E extends Comparable 부터 한 번 분석해보자. 

extends는 앞서 말했듯 extends 뒤에오는 타입이 최상위 타입이 되고, 해당 타입과 그에 대한 하위 타입이라고 했다. 그럼 역으로 생각해보면 이렇다. E 객체는 반드시 Comparable을 구현해야한다는 의미 아니겠는가?

 

예제로 보면 이렇다는 것이다.

 

 
public class SaltClass <E extends Comparable<E>> { ... }
 
 
 
public class Student implements Comparable<Student> {
 
@Override
 
public int compareTo(Person o) { ... };
 
}
 
 
 
public class Main {
 
public static void main(String[] args) {
 
SaltClass<Student> a = new SaltClass<Student>();
 
}
 
}

 

이렇게만 쓴다면 E extends Comparable<E> 까지만 써도 무방하다.

즉, SaltClass의 E 는 Student 이 되어야 하는데, Comparable<Person> 의 하위 타입이어야 하므로 거꾸로 말해 Comparable을 구현해야한다는 의미인 것이다.

 

 

그러면 왜 Comparable<E> 가 아닌 <? super E> 일까?

잠깐 설명했지만, super E는 E를 포함한 상위 타입 객체들이 올 수 있다고 했다.

 

만약에 위의 예제에서 학생보다 더 큰 범주의 클래스인 사람(Person)클래스를 둔다면 어떻게 될까? 한마디로 아래와 같다면?

 
public class SaltClass <E extends Comparable<E>> { ... } // Error가능성 있음
 
public class SaltClass <E extends Comparable<? super E> { ... } // 안전성이 높음
 
 
 
public class Person {...}
 
 
 
public class Student extends Person implements Comparable<Person> {
 
@Override
 
public int compareTo(Person o) { ... };
 
}
 
 
 
public class Main {
 
public static void main(String[] args) {
 
SaltClass<Student> a = new SaltClass<Student>();
 
}
 
}

 

쉽게 말하면 Person을 상속받고 Comparable 구현부인 comparTo에서 Person 타입으로 업캐스팅(Up-Casting) 한다면 어떻게 될까?

 

만약 <E extends Comparable<E>>라면 SaltClass<Student> a 객체가 타입 파라미터로 Student를 주지만, Comparable에서는 그보다 상위 타입인 Person으로 비교하기 때문에 Comparable<E>의 E인 Student보다 상위 타입 객체이기 때문에 제대로 정렬이 안되거나 에러가 날 수 있다.

 

그렇기 때문에 E 객체의 상위 타입, 즉 <? super E> 을 해줌으로써 위와같은 불상사를 방지할 수가 있는 것이다.

 

이 부분은 중요한 것이 이후 필자가 PriorityQueue와 TreeSet 자료구조를 구현할 것인데, 이 부분을 이해하고 있어야 가능하기 때문에 조금은 어렵더라도 미리 언급하고 가려한다.

 

 

<E extends Comparable<? super E>> 에 대해 설명이 조금 길었다. 이 긴 내용을 한 마디로 정의하자면 이렇다.

"E 자기 자신 및 조상 타입과 비교할 수 있는 E"

 

 

 

 

 

3. <?> (와일드 카드 : Wild Card)

 

마지막으로 와일드 카드다. 

이 와일드 카드 <?> 은 <? extends Object> 와 마찬가지라고 했다. Object는 자바에서의 모든 API 및 사용자 클래스의 최상위 타입이다. 한마디로 다음과 같은 의미나 마찬가지다.

 
public class ClassName { ... }
 
public class ClassName extends Object { ... }

 

우리가 public class ClassName extends Object {} 를 묵시적으로 상속받는 것이나 다름이 없다.

 

한마디로 <?>은 무엇이냐. 어떤 타입이든 상관 없다는 의미다. 당신이 String을 받던 어떤 타입을 리턴 받던 알빠 아니라는 조금 과격한 얘기..

 

이는 보통 데이터가 아닌 '기능'의 사용에만 관심이 있는 경우에 <?>로 사용할 수 있다.

 

 

 

 

 

 

 

 


 

참고

 

https://jackjeong.tistory.com/100

 

[Java] Shallow copy(얕은 복사) vs Deep copy(깊은 복사)

안녕하세요~ 잭코딩입니다! 이번 내용에서는 Shallow copy(얕은 복사)와 Deep copy(깊은복사)를 살펴봅시다 코드를 짜다보면 객체를 복사해야할 경우가 생깁니다 이 때 실수로 복사를 잘못하면 큰 이

jackjeong.tistory.com

https://woovictory.github.io/2020/04/22/Java-Array-Copy/

 

[Java] 얕은 복사와 깊은 복사

배열의 복사와 관련해서는 얕은 복사와 깊은 복사의 차이점을 알아야 한다.

woovictory.github.io

https://st-lab.tistory.com/153

 

자바 [JAVA] - 제네릭(Generic)의 이해

정적언어(C, C++, C#, Java)을 다뤄보신 분이라면 제네릭(Generic)에 대해 잘 알지는 못하더라도 한 번쯤은 들어봤을 것이다. 특히 자료구조 같이 구조체를 직접 만들어 사용할 때 많이 쓰이기도 하고

st-lab.tistory.com

 

 
728x90

'JAVA(자바)' 카테고리의 다른 글

[자바] Eclipse 실행 가능한 JAR 파일 만들기  (0) 2022.07.07
[Java 07] 문자형 - char  (0) 2022.03.10
아스키코드, 유니코드  (0) 2022.03.06
자바 배열 공부[미해결]  (0) 2022.03.06
[java] Date DateTime TimeStamp  (0) 2022.02.19

댓글