null이 들어있지 않은 경우 뿐만 아니라, 전달이 잘 되지 않았을 때도 null이 들어올 수 있기 때문에 Optional 사용
NullPointerException
존재하지 않는 데이터의 내부에 접근하려고 할 때 발생하는 예외
런타임 예외로, 컴파일러가 예외를 확인해주지 않는 예외
public class Student {
// 속성
private String name;
// 생성자
// 기능
public String getName() {
return this.name;
}
}
public class Camp {
// 속성
private Student student;
// 생성자
// 기능: ⚠️ null 을 반환할 수 있는 메서드
public Student getStudent() {
return student;
}
public void setStudent(Student student) {
this.student = student;
}
}
public class Main {
public static void main(String[] args) {
Camp camp = new Camp();
Student student = camp.getStudent(); // ⚠️ student 에는 null 이 담김
// ⚠️ 아래 코드에서 NPE 발생! 컴파일러가 잡아주지 않음
String studentName = student.getName(); // 🔥 NPE 발생 -> 프로그램 종료
System.out.println("studentName = " + studentName);
}
}
NullPointerException 해결 방법
조건문으로 null인지 확인 후 코드 진행
-> 객체의 규모가 커질수록 처리할 과정이 많아서 현실적으로 힘듦
public class Main {
public static void main(String[] args) {
Camp camp = new Camp();
Student student = camp.getStudent();
String studentName;
if (student != null) { // ⚠️ 가능은하지만 현실적으로 어려움
studentName = student.getName();
} else {
studentName = "등록된 학생 없음"; // 기본값 제공
}
System.out.println("studentName = " + studentName);
}
}
Optional 사용
null일 가능성이 있는 변수를 `Optional.ofNullable(변수)`로 감싸서 반환 (Optional이라는 클래스의 ofNullable 메서드)
변수가 null이면 null을 감싸서 Optional 객체로 반환
변수가 null이 아니면 그 데이터를 감싸서 Optional 객체로 반환 (어쨌든 객체화)
이 메서드의 반환값은 Optional 객체가 되기 때문에 메서드의 반환타입을 `Optional<반환타입>`으로 지정
import java.util.Optional;
public class Camp {
// 속성
private Student student;
// 생성자
// 기능
// ✅ null 이 반환될 수 있음이 명확하게 표시됨
public Optional<Student> getStudent() {
return Optional.ofNullable(student);
}
public void setStudent(Student student) {
this.student = student;
}
}
Optional 클래스의 메서드
1. `isPresent()`
해당값이 null인지 데이터값이 들어있는지 확인해주는 함수
반환값은 boolean (null이면 false 반환)
2. `get()`
안에 들어있는 데이터 반환
isPresent()의 반환값을 flag로 사용해서 데이터를 반환하기 위해 isPresent랑 같이 사용 가능
public class Main {
public static void main(String[] args) {
Camp camp = new Camp();
// isPresent() 활용시 true 를 반환하고 싶을때 활용
// Student newStudent = new Student();
// camp.setStudent(newStudent);
// Optional 객체 반환받음
Optional<Student> studentOptional = camp.getStudent();
// Optional 객체의 기능 활용
boolean flag = studentOptional.isPresent(); // false 반환
if (flag) {
// 존재할 경우
Student student = studentOptional.get(); // ✅ 안전하게 Student 객체 가져오기
String studentName = student.getName();
System.out.println("studentName = " + studentName);
} else {
// null 일 경우
System.out.println("학생이 없습니다.");
}
}
}
배열이 가진 고정 크기의 단점을 극복하기 위해 객체들을 쉽게 삽입, 삭제, 검색할 수 있는 가변 크기의 컨테이너이다.
java.util 패키지는 다양한 컬렉션 인터페이스와 컬렉션 클래스를 제공한다.
컬렉션 클래스는 개발자가 바로 사용할 수 있는 것들로서, Vector<E>와 ArrayList<E> 클래스는 가변 크기의 배열을 구현하며, LinkedList<E>는 노드들이 링크로 연결되는 리스트를 구현한다.
Stack<E>는 스택을 구현하며, HashSet<E>은 집합을 구현한다.
이들은 모두 Collection<E> 인터페이스를 상속받고, 단일 클래스의 객체만들을 요소로 다루는 공통점이 있다.
이와 달리 HashMap<K, V> 클래스는 key(K)와 value(V)의 쌍으로 이루어지는 데이터를 저장하고 키로 쉽게 검색하도록 만든 컬렉션이다.
특징
제네릭이라는 기법으로 만들어져 있다.
컬렉션 클래스의 이름에는 <E>,<K>,<V> 등이 항상 포함되는데 이를 '타입 매개변수'라고 한다.
Vector<E>에서 E 대신 Integer와 같이 구체적인 타입을 지정하면 Vector<Integer>는 정수 값만 저장하는 벡터로 사용할 수 있다.
특정 타입만 다루지 않고 여러 종류의 타입으로 변신할 수 있도록, 컬렉션을 일반화시키기 위해서는 element라는 의미의 <E>를 사용한다.
그런 의미로 E를 일반화 시킨 타입 혹은 제네릭 타입이라고 부른다.
컬렉션의 요소는 객체들만 가능하다.
int, char, double 등 기본 타입의 데이터는 원칙적으로 컬렉션의 요소로 불가능하고, 래퍼형 데이터만 가능하다.
예를 들어, Vector<int> v = new Vector<int>();는 컴파일 오류가 발생하고, Vector<Integer> v = new Vector<Integer>(); 는 정상적으로 코드가 실행된다.
하지만오토박싱(Auto-Boxing)과오토언박싱(Auto-Unboxing)덕분에, 기본형 int 데이터를 컬렉션의 요소로추가하거나가져오는것은 가능하다.
인터페이스
특징
구현체
List
순서 유지, 중복 허용
ArrayList
Set
순서 없음, 중복 불가
HashSet
Map
키-값 구조, 키 중복 불가
HashMap
Vector<E>
배열을 가변 크기로 다룰 수 있도록 하고, 객체의 삽입, 삭제, 이동이 쉽도록 구성한 컬렉션 클래스.
벡터 생성
Vector<Integer> v = new Vector<Integer>();
// 레퍼런스 변수 선언과 벡터 생성 분리한 코드
Vector<String> stringVector;
stringVector = new Vector<String>();
주요 메소드
boolean add(E element) // 벡터 맨 뒤에 element 추가
void add(int index, E element) // index 인덱스에 element 삽입, 해당 위치부터의 요소들이 하나씩 뒤로 밀림
int capacity() // 벡터의 현재 용량 리턴
boolean addAll(Collection<? extends E> c) // 컬렉션 c의 모든 요소를 벡터 맨 뒤에 추가
void clear() // 벡터의 모든 요소 삭제
boolean contains(Object o) // 지정된 객체 o를 벡터가 포함하면 true 리턴
E elementAt(int index) // 인덱스 index의 요소 리턴
E get(int index) // 인덱스 index의 요소 리턴
int indexOf(Object o) // o와 같은 첫 번째 요소의 인덱스 리턴, 없으면 -1 리턴
boolean isEmpty() // 벡터가 비어 있으면 true 리턴
E remove(int index) // 인덱스 index의 요소 삭제. 해당 위치 이후의 요소들이 하나씩 앞으로 옮겨짐
boolean remove(Object o) // 객체 o와 같은 첫 번째 요소를 벡터에서 삭제
void removeAllElements() // 벡터의 모든 요소를 삭제하고 크기를 0으로 만듦
int size() // 벡터가 포함하는 요소 개수 리턴
Object[] toArray() // 벡터의 모든 요소를 포함하는 배열 리턴
벡터에 요소 삽입(add 메소드)
// 정수 5를 Wrapper 클래스로 객체화하여 삽입
v.add(Integer.valueOf(5));
// 자동 박싱 기능을 활용
v.add(5); // 5 -> new Integer(5)로 자동 박싱
//null 삽입
v.add(null);
자동 박싱에 의해 int 타입의 정수는 자동으로 Integer 객체로 변환되어 삽입된다.
그러나 위의 벡터 생성 코드에서 벡터를 생성할 때 Integer로 벡터 타입을 지정해줬기 때문에, int형이 아닌 데이터 타입을 자동 박싱으로 삽입하면 컴파일 오류가 발생하거나, 컴파일 오류가 발생해야 하지만 컴파일 타임에서 이 오류를 잡아내지 못할 수도 있다.
벡터의 요소 알아내기(get 메소드)
Vector<Integer> v = new Vector<Integer>();
Integer obj = v.get(1); // 첫 번째 인덱스에 있는 Integer 객체 get
int i = obj.intValue(); // obj에 있는 정수 알아냄
// 자동 언박싱 활용
int i = v.get(1);
자바의 제네릭 컬렉션을 사용한 객체 생성 문법의 진화
// Java 7 이전
Vector<Integer> v = new Vector<Integer>();
// Java 7 ~
Vector<integer> v = new Vector<>(); // 타입 매개변수 생략 시 컴파일러가 추론하여 타입 지정
// Java 10 ~
var v = new Vector<Integer>(); // var 키워드 도입, 컴파일러가 추론하여 타입 지정
ArrayList<E>
ArrayList는 Vector 클래스와 거의 동일하다.
크게 다른 점은 ArrayList는 스레드 간에 동기화를 지원하지 않기 때문에, 다수의 스레드가 동시에 ArrayList에 요소를 삽입하거나 삭제할 때 ArrayList의 데이터가 훼손될 우려가 있다.
하지만 멀티스레드 동기화를 위한 시간 소모가 없기 때문에, ArrayList는 Vector보다 속도가 빨라, 단일 스레드 응용에는 더 효과적이다.
add(index, element)를 통해 요소를 삽입할 때 index값보다 ArrayList에 들어 있는 요소의 개수가 작으면 예외가 발생한다.
void removeIf(람다식) // 람다식을 사용해서 특정 조건에 부합하는 요소들을 삭제 (Java 8 이상)
Iterator<E>
Vector, ArrayList, LinkedList, Set과 같이 요소가 순서대로 저장된 컬렉션에서 요소를 순차적으로 검색할 때는 java.util 패키지의 Iterator<E> 인터페이스를 사용하면 편리하다.
이때 Iterator<E>의 E는 컬렉션의 타입 매개변수와 동일하게 지정해야 한다.
Iterator 객체 생성
// 벡터 생성
Vector<Integer> v = new Vector<Integer>();
// 벡터 요소 순차적으로 검색하기 위해 Iterator 객체 생성
Iterator<Integer> it = v.iterator();
// next() 메소드를 사용하여 요소 순차 검색
while(it.hasNext()) {
int n = it.next();
}
주요 메소드
boolean hasNext() // 방문 요소가 남아 있으면 true 리턴
E next() // 다음 요소 리턴
void remove() // 마지막으로 리턴된 요소 제거
HashSet<E>
순서를 보장하지 않는 자료구조이기 때문에, 인덱스로 데이터에 접근할 수 없다.
따라서 HashSet은 get()을 지원하지 않는다.
// Set 을 구현한 HashSet
HashSet<String> uniqueNames = new HashSet<>();
// ✅ 추가
uniqueNames.add("Spartan");
uniqueNames.add("Steve");
uniqueNames.add("Isac");
uniqueNames.add("1");
uniqueNames.add("2");
// ⚠️ 순서를 보장 안함
System.out.println("uniqueNames = " + uniqueNames);
uniqueNames.get(0); // ❌ get 사용 불가
// ⚠️ 중복 불가
uniqueNames.add("Spartan");
System.out.println("uniqueNames = " + uniqueNames);
// ✅ 제거
uniqueNames.remove("Spartan");
System.out.println("uniqueNames = " + uniqueNames);
HashMap<K, V>
해시맵은 내부에 '키'와 '값'을 저장하는 자료 구조를 가지고, 다음과 같이 put(), get() 메소드를 이용하여 요소를 삽입하거나 검색한다.
put을 통해 키와 값을 입력하면 키를 이용하여 해시 함수를 실행하고 해시 함수가 리턴하는 위치에 키와 값을 저장한다.
get은 다시 키를 이용하여 동일한 해시 함수를 실행하여 값이 저장된 위치를 알아내어 값을 리턴한다.
이렇게 해시 맵은 해시 함수를 통해 키와 값이 저장되는 위치를 결정하므로, 사용자는 그 위치를 알 수 없고, 삽입되는 순서와 들어 있는 위치 또한 관계가 없다.
키로 데이터를 탐색하기 때문에 키는 중복될 수 없고, 값은 중복 가능하다.
null도키로사용가능하지만, HashMap에서는오직하나의 null 키만허용한다.
해시맵은 Vector<E>, ArrayList<E>와 달리 요소의 삽입 삭제 시 다른 요소들의 위치 이동이 필요 없다.
그리고 요소 검색이 더욱 빠르다. 해시맵의 get(key) 메소드가 호출되면 해시 함수가 key가 저장된 위치를 단번에 찾아내므로, Vector<E>나 ArrayList<E>처럼 모든 요소들을 하나씩 비교하는 시간 낭비가 전혀 없다.
하지만 해시맵은 인덱스를 이용해서 요소에 접근할 수 없고 오직 '키'로만 검색해야 한다.
그러므로 해시맵은 빠른 삽입과 검색이 필요한 응용에 적합하다.
키는 hashCode()와 equals()를기반으로저장되므로 hashCode()와 equals()가올바르게구현된객체를키로사용해야 한다.
예를들어, HashMap<MyClass, String>을사용할때 MyClass에서 hashCode()와 equals()를제대로오버라이딩해야 한다.
// 해시맵 생성
HashMap<String, String> h = new HashMap<String, String>();
// 요소 삽입, 검색 예시
h.put("apple", "사과");
String kor = h.get("apple");
주요 메소드
void clear() // 해시맵 모든 요소 삭제
boolean containsKey(Object key) // 지정된 키를 포함하고 있으면 true 리턴
boolean containsValue(Object value) // 지정된 값에 일치하는 키가 있으면 true 리턴
V get(Object key) // 지정된 키의 값 리턴, 키가 없으면 null 리턴. Value의 V
boolean isEmpty() // 해시맵이 비어있으면 true flxjs
Set<K> keySet() // 해시맵의 모든 키를 담은 Set<K> 컬렉션 리턴
V put(K key, V value) // 키와 값 쌍을 해시맵에 저장
V remove(Object key) // 지정된 키를 찾아 키와 값 모두 삭제
int size() // 해시맵에 포함된 요소의 개수 리턴
해시맵의 전체 검색
Set<String> keys = h.keySet();
Iterator<String> it = keys.iterator();
while(it.hasNext()){
String key = it.next();
String value = h.get(key);
System.out.println( "(" + key + "," + value + ")" );
}
// Map 을 구현한 HashMap
HashMap<String, Integer> memberMap = new HashMap<>();
// ✅ 추가
memberMap.put("Spartan", 15);
memberMap.put("Steve", 15); // ✅ 값은 중복 가능
memberMap.put("Isac", 1);
memberMap.put("John", 2);
memberMap.put("Alice", 3);
// ⚠️ 순서 보장 안함
System.out.println("memberMap = " + memberMap);
// ⚠️ 키 중복 불가: 값 덮어쓰기 발생
memberMap.put("Alice", 5);
System.out.println("memberMap = " + memberMap);
// ✅ 조회: 15
System.out.println(memberMap.get("Steve"));
// ✅ 삭제 가능
memberMap.remove("Spartan");
System.out.println("memberMap = " + memberMap);
// ✅ 키 확인
Set<String> keys = memberMap.keySet();
System.out.println("keys = " + keys);
// ✅ 값 확인
Collection<Integer> values = memberMap.values();
System.out.println("values = " + values);
Iterator의 필요성
Iterator는 요소가 순서대로 저장된 컬렉션에서 순차 검색을 위한 클래스인데, 반복문으로 충분히 Iterator를 대체할 수 있지 않을까?
ArrayList, Vector 등은 인덱스를 기반으로 데이터에 접근이 가능하기 때문에 반복문을 통해 충분히 순차 검색이 가능하다.
하지만 Set, Map과 같이 순서가 없거나, 인덱스 없이 키와 값으로만 다루어지는 경우는 Iterator를 통해 순차적인 접근을 해야 한다.
정리하자면, Iterator는순서를 보장하지 않는 컬렉션에서도 next()와 hasNext() 메소드를 사용하여 각 데이터를 순차적으로 탐색할 수 있다.
Set, Map과 같은 컬렉션도 순차적인 탐색이 가능하므로 이는 곧일관적인 방법으로 다양한 컬렉션을 탐색할 수 있다는 장점으로 이어진다.
마지막으로Iterator의 remove() 메소드를 사용하면 반복 중에도 안전하게 요소를 삭제할 수 있다.
Set, HashMap의 순차적 탐색 원리
Set과 HashMap이 내부적으로 순서가 보장되지 않음에도 Iterator를 사용하여 순차적으로 요소를 탐색 가능한 이유는 해당 자료구조의 구현 방식 때문이다.
두 자료구조는 모두 해시 테이블을 기반으로 구현되어 있다.
해시 값을 사용해 데이터를 저장하고 검색하기 때문에 요소가 삽입된 순서와는 관계없이 데이터가 저장된다.
(LinkedHashSet, LinkedHashMap 같은 경우는 삽입 순서가 보장되어 순서를 유지한다.)
Iterator는 순차적으로 이 자료구조의 요소에 접근할 수 있게 해주지만 그 순서는 자료구조가 보장하는 순서와는 무관하다.
즉, 각 요소의 순서가 보장되지는 않지만, 각 요소에 대한 접근은 일관되게 진행된다는 것이다.
그러므로 요소를 순차적으로 검색할 때, 그 순서는 자료구조 내부의 구현 방식에 따라 달라질 수 있다.
LinkedList<E>
LinkedList는 요소들을 양방향으로 연결하여 관리한다는 점을 제외하면 Vector, ArrayList와 거의 같다.
LinkedList는 맨 앞과 맨 뒤를 가리키는 head, tail 레퍼런스를 가지고 있어, 맨 앞이나 맨 뒤, 중간에 요소의 삽입이 가능하며, 인덱스를 이용하여 요소에 접근할 수도 있다.
Collections 클래스 활용
java.util 패키지에 포함된 Collections 클래스는 컬렉션을 다루는 유용한 여러 메소드를 지원한다.
Collections 클래스의 메소드는 모두 static 타입이므로 Collections 객체를 생성할 필요는 없다.
이 유틸리티 메소드들은 인자로 컬렉션 객체를 전달받아 처리한다.
sort() // 컬렉션에 포함된 요소들의 정렬
reverse() // 요소를 반대 순서로 정렬
max() // 요소들의 최대값 탐색
min() // 요소들의 최소값 탐색
binarySearch() // 이진 검색 (오름차순 정렬)
제네릭 만들기
제네릭 클래스 생성
public class MyClass<T> { // 제네릭 클래스 MyClass, 타입 매개변수 T
T val;
void set(T a) {
val = a;
}
T get() {
return val;
}
}
제네릭 클래스에 대한 레퍼런스 변수 선언
MyClass<String> s; // <T>를 String으로 구체화
구체화(specialization)
제네릭 클래스에 구체적인 타입을 대입하여 구체적인 객체를 생성하는 과정을 구체화라고 하며, 이 과정은 자바 컴파일러에 의해 이루어진다.
제네릭 클래스 내에서 제네릭 타입을 가진 객체의 생성은 허용되지 않는다.
MyClass<String> s = new MyClass<String>(); // 제네릭 타입 T를 String으로 구체화
MyClass<Integer> n = new MyClass<Integer>(); // 제네릭 타입 T를 Integer로 구체화
제네릭 클래스 내에서 제네릭 타입의 객체 생성 불가능한 이유
아래와 같은 코드에서 컴파일러가 MyVector<E> 클래스의 new E() 라인을 컴파일할 때, E에 대한 구체적인 타입을 알 수 없어, 호출될 생성자를 결정할 수 없고, 또한 객체 생성 시 어떤 크기의 메모리를 할당해야 하는지 전혀 알 수 없기 때문이다.
public class MyVector<E> { // 제네릭 클래스
E create() {
E a = new E(); // 제네릭 타입 객체 생성 -> 컴파일 오류
return a;
}
}
만약 다양한 타입의 객체를 위한 제네릭 클래스를 선언해야 한다면 아래와 같이 Object 타입의 객체를 생성하는 생성자 코드를 작성하면 된다.
아래의 코드는 메인 클래스에서 String 타입의 스택과, Integer 타입의 스택을 생성하여 데이터를 삽입, 삭제하는 코드이다.
class GStack<T> { // 제네릭 스택 선언
int top;
Object GStack() { // 생성자
top = 0;
stck = new Object [10]; // 객체 형식의 배열 생성
}
public void push(T item) {
if(top==10) return;
stck[top] = item;
top++;
}
public T pop() {
if(top==0) return null;
top--;
return (T)stck[top]; // 함수를 호출한 객체의 타입을 반환하기 위해 타입 매개변수 타입으로 캐스팅
}
}
public class MyClass {
public static void main(String[] args) {
GStack<String> stringStack = new GStack<String>(); // String 타입 객체 생성
stringStack.push("seoul");
stringStack.push("busan");
for(int n=0; n<2; n++) System.out.println(stirngStack.pop());
GStack<Integer> intStack = new GStack<Integer>(); // Int 타입 객체 생성
intStack.push(1);
intStack.push(3);
for(int n=0; n<2; n++) System.out.println(intStack.pop());
}
}
제네릭 - 배열
제네릭 타입을 사용하는 클래스나 인터페이스는 배열로 선언할 수 없다.
예를 들어, List<T>[] 같은 배열은 선언할 수 없다.
왜냐하면 자바에서 제네릭은 타입 소거가 일어나기 때문이다.
반면에, 배열 요소가 제네릭 타입인 배열은 선언할 수 있다.
예를 들어, T[] 같은 배열은 가능하다.
정리하자면 제네릭 타입을 사용한 배열의 요소는 선언할 수 있지만, 배열 자체를 제네릭 타입으로 선언하는 것은 불가능하다.
타입 소거란?
제네릭을 사용한 코드에서 중요한 개념으로, 제네릭 타입이 컴파일 시에 실제 타입 정보가 사라져 Object로 처리되고, 런타임에는 원래 타입이 아닌 그에 대응하는 일반 타입으로 변환되는 과정을 말한다.
타입 소거는 String, Integer 등으로 구체화된 제네릭의 경우에도 동일하게 발생한다.
그렇다면 왜 굳이 제네릭 타입을 구체화해야 할까 의문이 들겠지만, 컴파일러는 여전히 타입 검사를 통해 개발자가 구체화한 타입이 아닌 데이터의 삽입을 방지해주기 때문에 타입 안전성을 제공하는 것이다.
제네릭의 장점
- 동적으로 타입이 결정되지 않고, 컴파일 시에 타입이 결정되므로 보다 안전한 프로그래밍 가능
- 런타임에 타입 충돌 문제 방지
- 개발 시 타입 캐스팅 절차 불필요
- ClassCastException 방지 (객체를 호환되지 않는 클래스로 형변환할 때 발생하는 예외 방지)
System을 비롯하여 문자열, 수학 함수, 입출력 등과 같은 프로그래밍에 필요한 기본적인 클래스와 인터페이스 제공
- java.util
날짜, 시간, 벡터, 해시맵 등의 유틸리티 클래스와 인터페이스 제공
- java.io
키보드, 모니터, 프린터, 파일 등에 입출력하는 클래스와 인터페이스 제공
- java.awt / java.swing
자바 AWT(Abstract Windowing Toolkit)와 swing 패키지로서 GUI 프로그래밍에 필요한 클래스와 인터페이스 제공
Object 클래스
java.lang 패키지에 속한 클래스로, 모든 클래스에 강제로 귀속된다.
계층 구조 상 최상위 클래스이므로 모든 객체가 상속받아 사용할 공통 기능이 구현되어 있다.
주요 메소드
boolean equals(Object obj)
// obj가 가리키는 객체와 현재 객체의 내용이 같으면 true 리턴
Class getClass()
// 현 객체의 런타임 클래스 리턴
int hashCode()
// 현 객체에 대한 해시 코드 값 리턴
String toString()
// 현 객체에 대한 문자열 표현 리턴
void notify()
// 현 객체에 대해 대기하고 있는 하나의 스레드를 깨운다
void notifyAll()
//현 객체에 대해 대기하고 있는 모든 스레드를 깨운다
void wait()
// 다른 스레드가 깨울 때까지 현재 스레드를 대기하게 한다
equals(obj) 예시 코드
String a = new String("Hello");
String b = new String("Hello");
if(a==b) System.out.println("a==b");
// 두 레퍼런스는 각각의 Hello라는 객체의 위치를 가리키므로 출력되지 않음
if(a.equals(b)) System.out.println("a와 b는 둘 다 Hello입니다");
// 두 레퍼런스는 같은 내용의 객체를 가지므로 출력됨
Wrapper 클래스
java.lang 패키지에서 제공된다.
Wrapper라는 클래스는 실제로 존재하지 않지만, 숫자나 문자같은 기본 타입의 데이터 값을 객체로 다룰 수 있도록 하는 Byte, Short, Integer, Long, Character, Double, Float, Boolean 클래스를 모두 합쳐 Wrapper 클래스라고 통칭한다.
기본 타입의 값을 Wrapper 객체로 변환하는 것을 박싱(boxing)이라고 하고, 반대의 경우는 언박싱(unboxing)이라고 한다.
변환하고자 하는 타입으로 선언되어 있는 레퍼런스 변수에 값을 할당하면 자동으로 박싱/언박싱이 가능하다.
객체 생성자(valueOf)
Integer i = Integer.valueOf(10); // 정수 10 객체화
Character c = Character.valueOf('c'); // 문자 'c' 객체화
Boolean b = Boolean.valueOf(true); // 불린 값 true 객체화
주요 메소드(극히 일부. p353)
int intValue() // int 타입으로 변환
static int parseInt(String s) // 문자열 s를 10진 정수로 변환
static int parseInt(String s, int radix) //radix로 지정된 진법의 정수로 변환
String 클래스
java.lang 패키지에 포함된 클래스로, 다양한 생성자를 통해 String 객체를 생성할 수 있도록 한다.
스트링 리터럴은 자바 내부에서 리터럴 테이블로 특별히 관리하여 동일한 리터럴을 공유시키지만(동일한 문자열을 여러개 생성하지 않고, 하나의 문자열을 여러 레퍼런스가 참조), 아래 예시 코드처럼 생성자를 통해 생성된 스트링은 new를 통해 생성되는 다른 객체와 동일하게 힙 메모리에 생성된다.
스트링 타입은 스트링 리터럴든 스트링 객체든 수정이 불가능하기 때문에 새로운 스트링 객체를 생성하고 기존 레퍼런스에 할당하는 방식으로 사용해야 한다.
그러므로 스트링 리터럴 테이블에서 리터럴을 공유하는 시스템이 문제없이 작동할 수 있다.
객체 생성자
String() // 빈 스트링 객체 생성
String(Char[] value) // 배열에 있는 문자들을 스트링 객체로 생성
String(String original) // 매개변수로 주어진 문자열과 동일한 내용의 스트링 객체 생성
String(StringBuffer buffer) // 매개변수로 주어진 스트링 버퍼의 문자열을 스트링 객체로 생성
String str1 = "abc"; // 스트링 리터럴
String str2 = new String("abc");
char data[] = {'a', 'b', 'c'};
String str3 = new String(data); //str2와 str3 모두 "abc" 문자열 객체
주요 메소드
char charAt(int index) // 스트링에서 index 위치에 있는 문자 리턴
int codePointAt(int index) // 스트링에서 index 위치에 있는 유니코드값 리턴
int compareTo(String anotherString) // 두 스트링을 사전 순으로 비교하여 두 스트링이 같으면 0, 현 스트링이 매개변수보다 먼저 나오면 음수, 아니면 양수 리턴
String concat(String str) // 현재 스트링 뒤에 str을 덧붙인 새로운 스트링 리턴
boolean contains(charSequence s) //s에 지정된 문자들을 포함하고 있으면 true 리턴
int length() // 스트링 길이(문자 개수) 리턴
String replace(Charsequence target, Charsequence replacement) // target이 지정한 문자들을 replacement가 지정한 문자들로 변경해서 리턴
String[] split(String regex) // 정규식 regex에 일치하는 부분을 중심으로 스트링을 분리하고, 각각 배열로 저장하여 리턴
String subString(int beginIndex) // beginIndex 인덱스부터 시작하는 서브 스트링 리턴
String toLowerCase() // 소문자로 변경 후 리턴
String toUpperCase() // 대문자로 변경 후 리턴
String trim() // 스트링 앞뒤의 공백 문자 제거한 스트링 리턴
StringBuffer 클래스
java.lang 패키지에 포함되어 있으며, String 클래스와 같이 문자열을 다룬다.
String 객체와는 다르게 StringBuffer 객체는 문자열을 저장하는 가변 버퍼를 가지고 있기 때문에 저장된 문자열의 수정이 가능하다.
객체 생성자
StringBuffer() // 초기 버퍼 크기가 16인 스트링 버퍼 객체 생성
StringBuffer(charSequence seq) // seq가 지정하는 문자들을 포함하는 스트링 버퍼 생성
StringBuffer(int capacity) // 지정된 초기 크기를 갖는 스트링 버퍼 생성
StringBuffer(String str) // 지정된 스트링으로 초기화된 스트링 버퍼 객체 생성
주요 메소드
StringBuffer append(String str) // str 스트링을 스트링 버퍼에 덧붙인다
StringBuffer append(StringBuffer sb) // sb 스트링 버퍼를 현재 스트링 버퍼에 덧붙인다
int capacity() // 스트링 버퍼의 현재 크기 리턴
StringBuffer delete(int start, int end) // start~end앞까지의 문자열 삭제
StringBuffer insert(int offset, String str) // str 스트링을 스트링 버퍼의 offset 위치에 삽입(해당 위치에 있던 기존 문자는 뒤로 밀림)
StringBuffer replace(int start, int end, String str) // start~end앞까지의 문자열을 str로 대치
StringBuffer reverse() // 스트링 버퍼 내의 문자들을 반대 순서로 변경
void setLength(int newLength) // 스트링 버퍼 내 문자열 길이보다 newLength로 재설정, 현재 길이보다 큰 경우 널 문자로 채우며 작은 경우 기존 문자열이 잘림
StringTokenizer 클래스
java.util 패키지에 포함되어 있으며, 하나의 문자열을 구분 문자(delimeter)를 기준으로 여러 개의 문자열(토큰)로 분리하기 위해 사용한다.
오라클은 StringTokenizer가 아닌 String 클래스의 split()을 사용하라고 권장하고 있다.
객체 생성자
StringTokenizer(String str) // str의 각 문자를 구분 문자로 하여 문자열 분리하는 스트링 토크나이저 생성
StringTokenizer(String str, String delim) // str을 delim을 구분 문자로 하여 분리하는 스트링 토크나이저 생성
StringTokenizer(String str, String delim, boolean returnDelims) // +returnDelims가 true이면 delim이 포함된 문자도 토큰에 포함
주요 메소드
int countTokens() // 분리한 토큰의 개수 리턴
boolean hasMoreTokens() // 다음 토큰이 있으면 true 리턴
String nextToken() // 들어있는 다음 토큰 리턴
Math 클래스
java.lang 패키지에 포함되어 있으며 기본적인 산술 연산을 제공한다.
모든 멤버 메소드는 static 타입이므로 Math.메소드명 으로 사용하면 된다.
주요 메소드
static double abs(double a) // 실수 a의 절대값 리턴
static double cos(double a) // 실수 a의 consine값 리턴
static double sin(double a) // 실수 a의 sine값 리턴
static double tan(double a) // 실수 a의 tangent값 리턴
static double exp(double a) // e^a값 리턴
static double ceil(double a) // 올림. a보다 크거나 같은 수 중에서 가장 작은 정수를 실수 타입으로 리턴
static double floor(double a) // 내림. a보다 작거나 같은 수 중에서 가장 큰 정수를 실수 타입으로 리턴
static double max(double a, double b) // 두 매개변수 중 큰 수 리턴
static double min(double a, double b) // 두 매개변수 중 작은 수 리턴
static double random() // 0.0보다 크거나 같고 1.0보다 작은 임의의 실수 리턴
static long round(double a) // 실수 a를 소수 첫째 자리에서 반올림한 정수를 long 타입으로 반환
static double sqrt(double a) // 실수 a의 제곱근 리턴
1~100까지의 랜덤한 정수를 얻으려면 random()*100+1 식을 사용한다.
random()*100은 0.0~99.9999...사이의 실수이고, 여기에 1을 더하면 1.0~100.9999..를 얻는다.
이 값을 (int)로 강제 타입 변환을 하면 정수 1~100까지의 랜덤한 수를 구할 수 있다.
Calendar 클래스
java.util 패키지에 있는 추상 클래스로서, 년, 월, 일, 요일, 시간, 분, 초, 밀리초까지 프로그램이 실행되는 동안 개발자가 기억하고자 하는 시간과 날짜 정보를 저장하거나, 아래의 필드를 인자로 get(), set()을 이용하여 날짜나 시간을 알아내거나 설정한다.
주의할 점은 Calendar 클래스를 통해 현재 컴퓨터의 시간을 바꾸지는 못한다.
필드
YEAR
MONTH
HOUR
HOUR_OF_DAY 24시 기준 시간
SECOND
DAY_OF_MONTH 날짜
DAY_OF_WEEK 요일
AM_PM 오전/오후 구분
MINUTE
MILLISECOND
객체 생성
Calendar now = Calendar.getInstance();
추상 클래스이기 때문에 new Calendar()가 아닌 getInstance() 메소드를 통해 객체를 생성한다.
getInstance()가 리턴한 now 객체는 현재 날짜(년도, 달)와 시간 정보를 가진다.
get(), set() 메소드 활용 예시
// get()
int year = now.get(Calendar.YEAR);
int month = now.get(Calendar.MONTH)+1; //1월은 0을 리턴하므로 +1
// set()
Calendar firstDate = Calendar.getInstance();
firstDate.clear(); // 현재 날짜, 시간 정보 지우기
firstDate.set(2016, 11, 25); // 2016년 12월 25일로 설정
firstDate.set(Calendar.HOUR_OF_DAY, 20); // 시간을 저녁 8시로 설정
java.util.Random
난수를 생성하는 클래스로,객체를 생성하여 사용
boolean nextBoolean()
boolean 타입 난수 반환
int nextInt()
int 타입 난수 반환
int nextInt(int n)
0~n 미만의 int 타입 난수 반환
long nextLong()
long 타입 난수 반환
void nextBytes(Byte[] bytes)
bytes 배열을 무작위 난수(-128~127) 로 채움 (배열 자체를 수정하므로 리턴 타입이 void)
- 슈퍼 클래스(부모클래스)의 생성자가 먼저 실행된 후, 서브 클래스(자식클래스)의 생성자가 실행된다.
- 서브 클래스에서 슈퍼 클래스의 생성자를 명시적으로 선택하지 않은 경우에 슈퍼 클래스의 기본 생성자를 실행하기 때문에, 슈퍼 클래스에서 기본 생성자 없이 매개변수 생성자만 있는 경우에는 아래와 같은 오류가 발생한다.
class A {
public A(int x) { // 기본 생성자 없어서 오류 발생
System.out.println("매개변수 생성자 A");
}
}
class B extends A {
public B() { // 상속으로 인해 A 생성자 호출
System.out.println("생성자 B");
}
}
public class ConstructorEx {
public static void main(String[] args) {
B b = new b();
}
}
// 에러 메세지
Implicit super constructor A() is undefined. Must explicitly invoke another constructor.
만약 슈퍼 클래스의 특정 생성자를 호출하고자 한다면 super()를 사용하면 된다.
단,부모 클래스가 먼저 선언되어야 부모 클래스의 멤버를 사용할 수 있기 때문에, super()는 반드시 생성자의 첫 라인에 사용되어야 한다.
class A {
public A() {
System.out.println("생성자 A");
}
public A(int x) {
System.out.println("매개변수 생성자 A");
}
}
class B extends A {
public B(int x) {
super(x); // 슈퍼클래스에서 int형 매개변수 하나를 가지는 생성자 호출
System.out.println("매개변수 생성자 B");
}
}
public class ConstructorEx {
public static void main(String[] args) {
B b = new b(5);
}
}
캐스팅 (=타입 변환)
- 업캐스팅
서브 클래스는 슈퍼 클래스의 속성을 상속받기 때문에, 서브 클래스 객체에 대한 레퍼런스를 슈퍼 클래스 타입으로 변환할 수 있다. 단, 업캐스팅한 레퍼런스로는 서브 클래스 객체 내 모든 멤버에 접근할 수는 없고, 슈퍼 클래스의 멤버에만 접근할 수 있다.
Person p;
Student s = new Student();
p = s; // 업캐스팅
Person pers = new Student(); // 업캐스팅
- 다운캐스팅
업캐스팅과는 달리, 다운캐스팅은 아래와 같이 명시적으로 타입 변환을 지정해야 한다.
업캐스팅한 레퍼런스를 다시 다운캐스팅하는 과정을 거치면 해당 레퍼런스는 이전처럼 서브 클래스 객체 내 모든 멤버에 접근할 수 있게 된다.
Student s = (Student)p; // 다운캐스팅
public class Main {
public static void main(String[] args) {
// 다형성 활용
Animal animal = new Cat();
animal.exist();
animal.makeSound();
Cat cat = (Cat) animal; // ✅ 다운캐스팅(부모Animal -> 자식Cat)
cat.scratch(); // ✅ 자식 클래스의 기능 활용 가능
}
}
다운캐스팅 시 주의사항과 instanceof 연산자
만약 여러 클래스가 상위 클래스를 상속하고, 각 클래스가 모두 상위 클래스로 업캐스팅한 레퍼런스를 가지고 있다고 가정해보자.
컴파일러는 다운캐스팅이 문법적으로 올바른지 여부만 검사해주기 때문에, 런타임시에 실제 어떤 객체가 변수에 할당되는지 검사해 주지 않는다.
컴파일 시점에는 오류 없이 통과되지만 런타임시점에 ClassCastException 이 발생할 가능성이 있다.
public class Main {
public static void main(String[] args) {
// 다운 캐스팅
Animal dog = new Dog();
// 문법적으로 잘못된건 아니라서 에러가 발생하지 않는다.
Cat cat1 = (Cat) dog; // ⚠️
cat1.scratch(); // ❌ 해당 라인을 실행할때만 에러 여부를 확인할 수 있다.
}
}
이때 상위 클래스의 객체에 어떤 클래스의 객체가 전달되어 있는지 쉽게 알 수 있는 방법이 바로 instanceof 연산자이다.
즉, instanceof 연산자는 레퍼런스가 가리키는 객체가 어떤 클래스 타입인지 구분할 수 있다.
주로 다운캐스팅 하기 전에 타입을 검사해서 ClassCastException 을 예방하는데 활용한다.
public class Main {
public static void main(String[] args) {
Animal animal2 = new Dog();
// ✅ 안전한 다운캐스팅(animal2 가 Cat 의 인스턴스 유형인지 확인)
if (animal2 instanceof Cat) {
Cat cat = (Cat) animal2;
cat.scratch();
} else {
System.out.println("객체가 고양이가 아닙니다.");
}
}
}
메소드 오버라이딩(=동적 바인딩)
- 슈퍼 클래스 메소드의 접근 지정자보다 접근의 범위를 좁혀서 오버라이딩할 수 없음 (public > protected > default > private)
- static, private, final로 선언된 메소드는 서브 클래스에서 오버라이딩할 수 없음
class Shape {
protected String name;
public void Paint() {
draw(); // 동적 바인딩으로 인해 Circle 출력
}
public void draw() {
System.out.println("Shape");
}
}
public class Circle extends Shape {
@override
void draw() { // ❌ default로 접근 지정자 좁히면 오버라이딩 불가!
System.out.println("Circle");
}
public static void main(String[] args) {
Shape b = new Circle();
b.paint();
}
}
정적 바인딩 (super 사용)
정적 바인딩에 사용되는 super는 자바 컴파일러에 의해 지원되는 슈퍼 클래스에 대한 레퍼런스이다.
class Shape {
protected String name;
public void Paint() {
draw();
}
public void draw() {
System.out.println("Shape");
}
}
public class Circle extends Shape {
@override
public void draw() {
System.out.println("Circle");
super.name = "Shape"; // 정적 바인딩
super.draw(); // 정적 바인딩
System.out.println(name);
}
public static void main(String[] args) {
Shape b = new Circle();
b.paint();
}
}
추상 클래스 (abstract)
추상 메소드란 선언은 되어 있으나 구현되어 있지 않은 메소드이다.
추상 클래스란 객체를 생성할 수 없는 클래스로, 하위 클래스에 구현을 강제하기 위해 사용한다.
abstract class Naming { // 추상 클래스
public abstract String getName(); // 추상 메소드
}
추상 클래스의 조건
1. 추상 메소드를 포함하는 클래스
2. 추상 메소드가 없지만 abstract로 선언한 클래스
위 두 가지 경우 모두 추상 클래스로 선언해야 한다.
추상 클래스의 구현
추상 클래스를 상속 받은 서브 클래스는 반드시 추상 메소드를 오버라이딩해야 한다.
추상 클래스의 목적
슈퍼 클래스에 선언된 모든 추상 메소드를 서브 클래스에서 오버라이딩하여 실행 가능한 코드로 구현하는 것이다.
즉, 추상 클래스는 추상 메소드를 통해 서브 클래스가 구현할 메소드를 명료하게 알려주는 인터페이스 역할을 하고 서브 클래스는 추상 메소드를 목적에 맞게 구현하는 다형성을 실현할 수 있다.
인터페이스
- 객체를 생성할 수 없고, 소프트웨어 모듈을 규격화하게 위해 필요한 개념
- 필드(멤버 변수)를 만들 수 없으므로, 자동으로 변수가 `public static final`로 선언된다.
인테페이스는 규격을 정의하기 위한 것이기 때문에, 변수는 최소한으로 사용
- 인터페이스 구현을 위한 클래스는 implements 키워드 사용
- 상속 가능 (인터페이스는 다중 상속 가능)
인터페이스 구성
상수
추상 메소드
default 메소드
private 메소드
static 메소드
interface PhoneInterface {
public static final int TIMEOUT = 10000; // 상수
int TIMEOUT2 = 10000;
public abstract void sendCall(); // 추상 메소드
void sendCall2();
public default void printLogo() { 생략 }; // 디폴트 메소드
default void printLogo() { 생략 };
}
class SamsungPhone implements PhoneInterface { // 인터페이스 구현 클래스
// PhoneInterface의 모든 추상메소드 구현
// 메소드 추가 작성 가능
}
너무 세분화된 동작을 각각의 세터로 기능하도록 해서 메인 클래스에서 중복/혼잡한 세터 사용
결과) 정신사나움
해결 방법) 특정범위동작을하나의세터안에넣고한번에실행하도록묶어주기
final
final 클래스 해당 클래스를 상속받을 수 없음
final class FinalClass {
public void show() {
System.out.println("This is a final class.");
}
}
// 오류: final 클래스는 상속할 수 없음
class ChildClass extends FinalClass {
public void show() {
System.out.println("This is a child class.");
}
}
final 메소드 오버라이딩 불가
class Parent {
public final void display() {
System.out.println("Parent display");
}
}
class Child extends Parent {
// 오류: final 메서드는 오버라이드할 수 없음
public void display() {
System.out.println("Child display");
}
}
final 필드 해당 필드는 상수가 되어 변경 불가
final int x = 10; // x는 10으로 고정됨
x = 20; // 컴파일 에러: final 변수는 값을 변경할 수 없음
final 매개변수 해당 파라미터 값 변경 불가
public void calculate(final int x) {
x = x + 5; // 오류: final 매개변수는 변경할 수 없음
}
불변객체 (Immutable Object)
내부 상태를 변경할 수 없는 객체
final을 속성에 활용
setter 없이 설계
변경이 필요할 경우, 새로운 객체 생성 필요
잘못된 불변객체 사용 사례
public class Circle {
final static double PI = 3.14159; // ✅ 직접 만든 원주율 상수
double radius; // ⚠️ final 로 선언되어 있지 않기 때문에 외부에서 변경 가능
Circle(double radius) {
this.radius = radius;
}
}
final Circle c1 = new Circle(2);
c1 = new Circle(3); // ❌
// final은 변수 c1이 한 번 참조한 객체는 다른 객체로 변경될 수 없음을 의미함 (참조 불변)
c1.radius = 3; // ⚠️ 내부 상태 변경 가능 (객체 자체가 불변이 아님)
올바른 불변객체 사용 사례
public final class Circle {
final static double PI = 3.14159;
final double radius; // ✅ final 로 선언해서 값이 변경되지 않도록
Circle(double radius) {
this.radius = radius;
}
}
불변객체의 값이 변경이 필요할 경우
1. 새로운 객체 생성
2. 클래스의 함수 내에서 새로운 객체 생성 -> 메인 클래스에서 마치 값을 변경하는 것처럼 사용 가능
public final class Circle {
public static final double PI = 3.14159;
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
// ✅ 반지름이 다른 새로운 Circle 생성 (불변 객체 유지)
public Circle changeRadius(double newRadius) {
return new Circle(newRadius); // 생성자 호출: 기존 객체 변경 X, 새 객체 생성
}
}