1. 열거형 (Enum)
열거형이란?
서로 연관된 상수들의 집합을 의미한다.
enum Seasons {SPRING, SUMMER, FALL, WINTER}
public class Main {
public static void main(String[] args) {
Seasons seasons = Seasons.SPRING;
switch (seasons) {
case SPRING:
System.out.println("봄");
break;
case SUMMER:
System.out.println("여름");
break;
case FALL:
System.out.println("가을");
break;
case WINTER:
System.out.println("겨울");
break;
}
}
}
//출력값
봄
열거형을 사용할 경우 여러 상수들을 보다 편리하게 선언하고 관리할 수 있게하며 상수명의 중복을 피하고 타입에 대한 안정성을 보장한다.
또한 같은 효과를 낼 수 있는 다른 코드에 반해 훨씬 더 간결하고 가독성이 좋은 코드를 작성할 수 있으며 switch문에서도 작동이 가능하다.
열거형의 사용
enum 열거형이름 { 상수명1, 상수명2, 상수명3, ...}
상수명은 관례적으로 대문자로 작성하며 앞에서부터 차례대로 0, 1, … 의 값이 할당된다.
열거형이름.상수명을 통해서 열거형에 선언된 상수에 접근이 가능하다.
2. 제네릭
타입을 구체적으로 지정하는 것이 아니라, 추후에 지정할 수 있도록 일반화해두는 것을 의미한다.
즉, 작성한 클래스 또는 메서드의 코드가 특정 데이터 타입에 얽매이지 않게 해둔 것을 의미한다.
제네릭 클래스 정의 및 사용
class 클래스명<T> {
private T item;
...
}
class Main {
public static void main(String[] args) {
클래스명<String> 인스턴스명 = new 클래스명<>("Hello");
클래스명<Integer> 인스턴스명 = new 클래스명<>(1);
}
제네릭 클래스를 정의할 때 클래스 변수에는 타입 매개변수를 사용할 수 없다.
static T item2; 와 같이 사용할 수 없다는 얘기다.
만약 같은 제네릭 클래스로 String 타입의 인스턴스와 Integer 타입의 인스턴스를 정의했다고 가정하면 두 인스턴스는 같은 변수를 공유하는 것이 아니다.
따라서 static이 붙은 변수 혹은 메서드에는 타입 매개변수를 사용할 수 없다.
제네릭 클래스를 사용할 때에도 다형성 적용이 가능하다.
class Flower { ... }
class Rose extends Flower { ... }
class RosePasta { ... }
class Basket<T> {
private T item;
public T getItem() {
return item;
}
public void setItem(T item) {
this.item = item;
}
}
class Main {
public static void main(String[] args) {
Basket<Flower> flowerBasket = new Basket<>();
flowerBasket.setItem(new Rose()); // 다형성 적용
flowerBasket.setItem(new RosePasta()); // 에러
}
}
위 코드에서 Rose 클래스는 Flower 클래스를 상속 받고 있다.
new Rose()를 통해 생성된 인스턴스는 Rose 타입이며 Basket<Flower>의 item에 할당될 수 있다.
반면, new RosePasta()를 통해 생성된 인스턴스는 RosePasta 타입이며 Flower 클래스와 무관하기 때문에 flowerBasket의 item에 할당될 수 없다.
제한된 제네릭 클래스
class Flower { ... }
class Rose extends Flower { ... }
class RosePasta { ... }
class Basket<T extends Flower> {
private T item;
...
}
class Main {
public static void main(String[] args) {
// 인스턴스화
Basket<Rose> roseBasket = new Basket<>();
Basket<RosePasta> rosePastaBasket = new Basket<>(); // 에러
}
}
extends 클래스명 을 사용하면 인스턴스화할 때 사용할 타입을 제한할 수 있다.
위 코드로 예를 들면 Flower클래스의 하위 클래스로만 인스턴스를 생성할 수 있다.
만약 특정 클래스를 상속받는 동시에 특정 인터페이스를 구현한 클래스만 타입으로 지정할 수 있도록 제한하려면 아래와 같이 &를 사용하면 된다.
다만 이러한 경우 클래스를 인터페이스보다 앞에 위치시켜야 한다.
interface Plant { ... }
class Flower implements Plant { ... }
class Rose extends Flower implements Plant { ... }
class Basket<T extends Flower & Plant> { // (1)
private T item;
...
}
class Main {
public static void main(String[] args) {
// 인스턴스화
Basket<Flower> flowerBasket = new Basket<>();
Basket<Rose> roseBasket = new Basket<>();
}
}
제네릭 메서드
클래스 내부의 특정 메서드만 제네릭으로 선언한 것을 제네릭 메서드라고 한다.
제네릭 메서드의 타입 매개변수 선언은 반환타입 앞에서 이루어지며 해당 메서드 내에서만 선언한 타입 매개변수를 사용할 수 있다.
class Basket {
...
public <T> void add(T element) {
...
}
}
제네릭 메서드의 타입 매개변수는 제네릭 클래스의 타입 매개변수와 별개의 것이다.
만약 아래와 같이 동일하게 T라는 타입 매개변수명을 사용한다고 해도 서로 다른 타입의 매개변수로 간주된다.
class Basket<T> { // 1 : 여기에서 선언한 타입 매개변수 T와
...
public <T> void add(T element) { // 2 : 여기에서 선언한 타입 매개변수 T는 서로 다른 것입니다.
...
}
}
이는 타입이 지정되는 시점이 서로 다르기 때문이다.
클래스명 옆에서 선언한 타입 매개변수는 클래스가 인스턴스화 될 때 타입이 지정되며,
제네릭 메서드의 타입 지정은 메서드가 호출될 때 이루어진다.
또한, 클래스 타입 매개변수와 달리 메서드 타입 매개변수는 static 메서드에서도 선언하여 사용할 수 있다.
제네릭 메서드를 정의하는 시점에서는 실제 어떤 타입이 입력되는지 알 수 없기 때문에 length()와 같은 String 클래스의 메서드는 제네릭 메서드를 정의하는 시점에 사용할 수 없다.
하지만 equals(), toString() 과 같은 Object 클래스의 메서드는 사용이 가능하다.
class Basket {
public <T> void print(T item) {
System.out.println(item.length()); // 불가
}
}
class Basket {
public <T> void getPrint(T item) {
System.out.println(item.equals("Kim coding")); // 가능
}
}
와일드카드
어떠한 타입으로든 대체될 수 있는 타입 파라미터를 의미하며, 기호 ? 로 사용이 가능하다.
와일드카드는 extends 와 super 키워드를 조합하여 사용하는 것이 일반적이다.
<? extends T> // 1
<? super T> // 2
1의 경우, 와일드카드에 상한 제한을 두는 것으로서 T와 T를 상속받는 하위 클래스 타입만 타입 파라미터로 받을 수 있도록 지정한다.
반면 2의 경우, 하한 제한을 두는 것으로 T와 T의 상위 클래스만 타입 파라미터로 받도록 한다.
<?>와 같이 extends 및 super 키워드와 조합하지 않은 와일드카드는 <? extends Object>와 같다.
즉, 모든 클래스 타입을 타입 파라미터로 받을 수 있음을 의미한다.
class PhoneFunction {
public static void call(User<? extends Phone> user) {
System.out.println("-----------------------------");
System.out.println("user.phone = " + user.phone.getClass().getSimpleName());
System.out.println("모든 Phone은 통화를 할 수 있습니다.");
}
public static void faceId(User<? extends IPhone> user) {
System.out.println("-----------------------------");
System.out.println("user.phone = " + user.phone.getClass().getSimpleName());
System.out.println("IPhone만 Face ID를 사용할 수 있습니다. ");
}
public static void samsungPay(User<? extends Galaxy> user) {
System.out.println("-----------------------------");
System.out.println("user.phone = " + user.phone.getClass().getSimpleName());
System.out.println("Galaxy만 삼성 페이를 사용할 수 있습니다. ");
}
public static void recordVoice(User<? super Galaxy> user) {
System.out.println("-----------------------------");
System.out.println("user.phone = " + user.phone.getClass().getSimpleName());
System.out.println("안드로이드 폰에서만 통화 녹음이 가능합니다. ");
}
}
public class Example {
public static void main(String[] args) {
PhoneFunction.call(new User<Phone>(new Phone()));
PhoneFunction.call(new User<IPhone>(new IPhone()));
PhoneFunction.call(new User<Galaxy>(new Galaxy()));
PhoneFunction.call(new User<IPhone12Pro>(new IPhone12Pro()));
PhoneFunction.call(new User<IPhoneXS>(new IPhoneXS()));
PhoneFunction.call(new User<S22>(new S22()));
PhoneFunction.call(new User<ZFlip3>(new ZFlip3()));
System.out.println("\\n######################################\\n");
// PhoneFunction.faceId(new User<Phone>(new Phone())); // X
PhoneFunction.faceId(new User<IPhone>(new IPhone()));
PhoneFunction.faceId(new User<IPhone12Pro>(new IPhone12Pro()));
PhoneFunction.faceId(new User<IPhoneXS>(new IPhoneXS()));
// PhoneFunction.faceId(new User<Galaxy>(new Galaxy())); // X
// PhoneFunction.faceId(new User<S22>(new S22())); // X
// PhoneFunction.faceId(new User<ZFlip3>(new ZFlip3())); // X
System.out.println("\\n######################################\\n");
// PhoneFunction.samsungPay(new User<Phone>(new Phone())); // X
// PhoneFunction.samsungPay(new User<IPhone>(new IPhone())); // X
// PhoneFunction.samsungPay(new User<IPhone12Pro>(new IPhone12Pro())); // X
// PhoneFunction.samsungPay(new User<IPhoneXS>(new IPhoneXS())); // X
PhoneFunction.samsungPay(new User<Galaxy>(new Galaxy()));
PhoneFunction.samsungPay(new User<S22>(new S22()));
PhoneFunction.samsungPay(new User<ZFlip3>(new ZFlip3()));
System.out.println("\\n######################################\\n");
PhoneFunction.recordVoice(new User<Phone>(new Phone()));
// PhoneFunction.recordVoice(new User<IPhone>(new IPhone())); // X
// PhoneFunction.recordVoice(new User<IPhone12Pro>(new IPhone12Pro())); // X
// PhoneFunction.recordVoice(new User<IPhoneXS>(new IPhoneXS())); // X
PhoneFunction.recordVoice(new User<Galaxy>(new Galaxy()));
// PhoneFunction.recordVoice(new User<S22>(new S22())); // X
// PhoneFunction.recordVoice(new User<ZFlip3>(new ZFlip3())); // X
}
}
3. 예외처리
예기치 않게 발생하는 에러에 대응할 수 있는 코드를 미리 사전에 작성하여 프로그램의 비정상적인 종료를 방지하고, 정상적인 실행 상태를 유지하기 위한 수단이다.
에러가 발생하는 원인은 크게 내부적인 요인과 외부적인 요인으로 구분된다.
내부적인 요인 중 발생 시점에 따라 컴파일 에러와 런타임 에러로 구분한다.
컴파일 에러
컴파일 할 때 발생하는 에러를 가리키며 주로 문법적인 문제를 가리키는 신택스(syntax) 오류로부터 발생하기 때문에 신택스 에러(Syntax Errors)라고 부르기도 한다.
자바 컴파일러가 오류를 감지하여 알려주기 때문에 상대적으로 쉽게 발견하고 수정할 수 있다.
런타임 에러
코드를 실행하는 과정, 즉 런타임 시에 발생하는 에러를 말한다.
주로 개발자가 컴퓨터가 수행할 수 없는 특정한 작업을 요청할 때 발생하며, 프로그램이 실행될 때 JVM에 의해 감지된다.
런타임 시 발생할 수 있는 프로그램 오류를 크게 에러(Error)와 예외(Exception)으로 구분한다.
에러란 복구하기 어려운 수준의 심각한 오류를 의미하며 대표적으로 메모리 부족(OutOfMemoryError)와 스택오버플로우(StackOverflowError) 등이 있다.
반면 예외는 잘못된 사용 또는 코딩으로 인한 오류로 코드 수정 등을 통해 수습이 가능한 오류를 지칭한다.
예외 클래스의 상속 계층도
자바에서는 예외가 발생하면 예외 클래스로부터 객체를 생성하여 해당 인스턴스를 통해 예외처리를 한다.
위 이미지로 확인할 수 있듯이 모든 예외의 최고 상위 클래스는 Exception 클래스이며 일반 예외 클래스와 실행 예외 클래스로 나누어진다.
try - catch문
예외 처리는 try - catch 블럭을 통해 구현이 가능하다.
try 블럭 안에는 예외가 발생할 가능성이 있는 코드를 삽입한다.
만약 작성한 코드가 예외 없이 정상적으로 실행되면 catch 블럭은 실행되지 않고 finally 블럭이 실행된다.
finally 블럭은 필수적으로 포함하지 않아도 되지만, 만약 포함하는 경우 예외 발생 여부와 상관 없이 항상 실행된다.
catch 블럭은 예외가 발생하는 경우 실행되는 코드이다.
try {
// 예외가 발생할 가능성이 있는 코드를 삽입
}
catch (ExceptionType1 e1) {
// ExceptionType1 유형의 예외 발생 시 실행할 코드
}
catch (ExceptionType2 e2) {
// ExceptionType2 유형의 예외 발생 시 실행할 코드
}
finally {
// finally 블럭은 옵셔널
// 예외 발생 여부와 상관없이 항상 실행
}
예외 전가
예외를 호출한 곳으로 다시 예외를 떠넘기는 방법도 있다.
메서드의 선언부 끝에 아래와 같이 throws 키워드와 발생할 수 있는 예외들을 쉼표로 구분하여 나열해주면 된다.
반환타입 메서드명(매개변수, ...) throws 예외클래스1, 예외클래스2, ... {
...생략...
}
예외를 의도적으로 발생시키기
throw 키워드를 사용하면 의도적으로 예외를 만들 수 있다.
public class ExceptionTest {
public static void main(String[] args) {
try {
Exception intendedException = new Exception("의도된 예외 만들기");
throw intendedException;
} catch (Exception e) {
System.out.println("고의로 예외 발생시키기 성공!");
}
}
}
//출력값
고의로 예외 발생시키기 성공!
try 블럭 안에서 intendedException이라는 예외를 만들어 throw 키워드를 이용해 발생시킨 후 catch 블럭에서 처리하는 과정이다.
4. 컬렉션 프레임워크(Collection Framework)
여러 데이터들을 그룹으로 묶어놓은 것을 컬렉션이라고 하며, 이러한 컬렉션을 다루는 데 편리한 메서드들을 미리 정의해놓은 것을 컬렉션 프레임워크라고 한다.
특정 자료 구조에 데이터를 추가, 삭제, 수정, 검색하는 등의 동작을 수행하는 편리한 메서드들을 제공한다.
주요 인터페이스로 List, Set, Map을 제공하는데 간단히 요약하면 아래와 같다.
List
데이터의 순서가 유지되며 중복 저장이 가능한 컬렉션을 구현하는 데에 사용된다.
Set
데이터의 순서가 유지되지 않으며 중복 저장이 불가능한 컬렉션을 구현하는 데에 사용된다.
Map
키(key)와 값(value)의 쌍으로 데이터를 저장하는 컬렉션을 구현하는 데에 사용된다.
데이터의 순서가 유지되지 않으며, 키는 중복 저장이 불가능하고 값은 중복 저장이 가능하다.
List<E>
배열과 같이 객체를 일렬로 늘어놓은 구조를 가지고 있다.
객체를 인덱스고 관리하기 때문에 객체를 저장하면 자동으로 인덱스가 부여되고, 인덱스로 객체를 검색, 추가, 삭제할 수 있는 등의 여러 기능을 제공한다.
List를 구현한 클래스에는 ArrayList, Vector, LinkedList, Stack 등이 있다.
ArrayList
객체가 인덱스로 관리된다는 점에서 배열과 유사하다.
배열은 생성될 때 크기가 고정되며, 크기를 변경할 수 없지만 ArrayList는 저장 용량을 초과하여 객체들이 추가되면 자동으로 저장 용량이 늘어나게 된다.
또한 리스트 계열 자료구조의 특성을 이어받아 데이터가 연속적으로 존재한다.
즉, 데이터의 순서를 유지한다.
ArrayList<타입 매개변수> 객체명 = new ArrayList<타입 매개변수>(초기 저장 용량);
ArrayList<String> container1 = new ArrayList<String>();
// String 타입의 객체를 저장하는 ArrayList 생성
// 초기 용량이 인자로 전달되지 않으면 기본적으로 10으로 지정됩니다.
ArrayList<String> container2 = new ArrayList<String>(30);
// String 타입의 객체를 저장하는 ArrayList 생성
// 초기 용량을 30으로 지정하였습니다.
특정 인덱스의 객체를 제거하면 바로 뒤 인덱스부터 마지막 인덱스까지 모두 앞으로 1씩 당겨지기 때문에 객체 삭제와 삽입이 빈번하게 일어날 경우 LinkedList를 사용하는 것이 좋다.
LinkedList
배열에는 모든 데이터가 연속적으로 존재하지만 LinkedList에는 불연속적으로 존재한다.
각 Node들은 자신과 연결된 이전 요소 및 다음 요소의 주소값과 데이터로 구성되어 있기 때문에 리스트의 중간에 삭제, 삽입이 이루어질 경우 참조값만 변경해주면 된다.
하지만 데이터를 검색할 때는 시작 인덱스부터 찾고자 하는 데이터까지 순차적으로 각 노드에 접근해야 하기 때문에 데이터 검색에 있어서는 비효율적이다.
결론적으로, 데이터 변경이 잦은 경우 LinkedList를 사용하고, 순차적으로 값을 삽입, 삭제하거나 탐색하는 경우 ArrayList를 사용하는 게 효율적이다.
Iterator
컬렉션에 저장된 요소들을 순차적으로 읽어오는 역할을 한다.
이러한 Iterator의 컬렉션 순회 기능은 Iterator 인터페이스에 정의되어 있으며, Collection 인터페이스에는 Iterator 인터페이스를 구현한 클래스의 인스턴스를 반환하는 메서드인 iterator()가 정의되어 있다.
즉, collection 인터페이스에 정의된 iterator()를 호출하면, Iterator 타입의 인스턴스가 반환된다.
따라서 Collection 인터페이스를 상속받는 List와 Set 인터페이스를 구현한 클래스들은 iterator() 메서드를 사용할 수 있다.
Iterator 인터페이스에 정의된 메서드
메서드 설명
hasNext() | 읽어올 객체가 남아 있으면 true를 리턴하고, 없으면 false를 리턴합니다. |
next() | 컬렉션에서 하나의 객체를 읽어옵니다. 이 때, next()를 호출하기 전에 hasNext()를 통해 읽어올 다음 요소가 있는지 먼저 확인해야 합니다. |
remove() | next()를 통해 읽어온 객체를 삭제합니다. next()를 호출한 다음에 remove()를 호출해야 합니다. |
Set<E>
요소의 중복을 허용하지 않고, 저장 순서를 유지하지 않는 컬렉션이다.
Set을 구현한 클래스는 대표적으로 HashSet, TreeSet이 있다.
HashSet
Set 인터페이스의 특성을 그대로 물려받은 가장 대표적인 컬렉션 클래스이다.
TreeSet
이진 탐색 트리 형태로 데이터를 저장하며, Set 인터페이스 특성은 그대로 유지된다.
이진 탐색 트리(Binary Search Tree)란 하나의 부모 노드가 최대 두 개의 자식 노드와 연결되는 이진 트리의 일종으로, 정렬과 검색에 특화된 자료 구조이다.
모든 왼쪽 자식의 값이 루트나 부모보다 작고, 모든 오른쪽 자식의 값이 루트나 부모보다 큰 값을 가지는 특징이 있다.
Map<K, V>
Map 인터페이스는 키(key)와 값(value)으로 구성된 객체를 저장하는 구조를 가지고 있으며 이 객체를 Entry 객체라고 한다.
키의 역할은 값을 식별하는 것이기 때문에 중복 저장이 불가능하지만 값은 중복 저장이 가능하다.
Map 인터페이스를 구현한 클래스는 대표적으로 HashMap, Hashtable, TreeMap, SortedMap 등이 있다.
HashMap
해시 함수를 통해 ‘키’와 ‘값’이 저장되는 위치를 결정하기 때문에 사용자는 그 위치를 알 수 없고, 삽입된느 순서와 위치 또한 관계가 없다.
이렇게 해싱(Hashing)을 사용하기 때문에 많은 양의 데이터를 검색하는 데 있어서 뛰어난 성능을 보인다.
HashMap<String, Integer> hashmap = new HashMap<>();
HashMap을 생성할 때는 이렇게 키와 값의 타입을 따로 지정해주어야 한다.
Hashtable
HashMap과 내부 구조가 동일하며, 사용 방법 또한 매우 유사하다.
멀티스레드 환경이 아니라면 HashMap보다 성능이 떨어진다는 단점이 있으며 key에 null을 허용하지 않는 차이점이 있다.
컬렉션 클래스 정리
'CodeStates_BE_44 > TIL' 카테고리의 다른 글
Day 17. [Java] 파일 입출력 (0) | 2023.03.09 |
---|---|
Day 16. [Java] 애너테이션, 람다, 스트림 (0) | 2023.03.08 |
Day 12. [Java] 객체지향 프로그래밍 심화_다형성, 추상화 (0) | 2023.02.28 |
Day 11. [Java] 객체지향 프로그래밍 심화_상속, 캡슐화 (0) | 2023.02.27 |
Day 10. [Java] 객체지향 프로그래밍 기초_생성자, 내부 클래스 (0) | 2023.02.24 |