자바는 함수적 프로그래밍을 위해 자바 8부터 람다식(Lambda Expression)을 지원한다. 람다식은 익명함수(anonymous function)을 생성하기 위한 식으로 객체지향 언어 보다는 함수지향 언어에 가까워 기존 자바 개발과 다르게 생각될 수 있다. 자바에서 람다식을 수용한 이유는 자바 코드가 매우 간결해지고, 컬렉션의 요소를 필터링하거나 매핑해서 원하는 결과를 쉽게 집계할 수 있기 때문이다. 람다식의 형태는 매개 변수를 가진 코드 블록이지만, 런타임시에는 익명 구현 객체를 생성한다.
(람다식 -> 매개 변수를 가진 코드 블록 -> 익명 구현객체)
//Runnable 인터페이스의 익명 구현 객체 생성하는 코드
Runnable runnable = new Runnable() {
public void run();
};
//Runnable 인터페이스의 익명 구현 객체 생성 람다식
Runnable runnable = () -> {...};
(타입 매개변수, ...) -> {실행문; ...} // 매개 변수의 이름은 사용자 정의
//기본
(int a) -> {System.out.println(a);}
//매개 변수가 1개 라면 ()와 타입을 생략 가능, 매개 변수 타입은 대입되는 값에 따라 자동으로 인식될 수 있기 때문에 람다식에서는 매개 변수의 타입을 일반적으로 언급하지 않음
a -> {System.out.prinln(a);}
//실행문이 하나일 때 {}를 생략 가능
a -> System.out.println(a)
//만약 매개 변수가 없다면 빈 괄호를 사용
() ->{실행문; ..}
//리턴하는 실행문은 다음과 같이 작성 가능
(x,y) -> {return x+y;}
//중괄호에 return문만 있을 경우 다음과 같이 작성 가능
(x,y) -> x+y
람다식의 형태는 매개 변수를 가진 코드 블록이기 때문에 마치 자바의 메소드를 선언하는 것처럼 보이나, 자바는 메소드를 단독으로 선언할 수 없고 항상 클래스의 구성 멤버로 선언하기 때문에 람다식은 단순히 메소드를 선언하는 것이 아니라 이 메소드를 가지고 있는 객체를 생성한다.
인터페이스 변수 = 람다식; //람다식으로 인터페이스의 익명 구현 객체를 생성
//(타겟 타입)
@FunctionalInterface
public interface MyFunctionalInterface {
public void method();
public void otherMethod(); // 컴파일 오류 발생
}
//함수적 인터페이스
@FunctionalInterface
public interface MyFunctionalInterface {
public void method();
}
//위 인터페이스를 타겟 타입으로 갖는 람다식
MyFunctionalInterface fi = () -> {...}
//참조 변수로 메소드를 호출하여 람다식의 중괄호를 실행시킴
fi.method();
public class MyFunctionalInterfaceEx{
public static void main(String[]args){
MyFunctionalInterface fi;
fi= () -> {
String str = "method call1";
System.out.println(str);
};
fi.method();
fi = () -> System.out.println("mehod call2");
fi.method();
}
}
@FunctionalInterface
public interface MyFunctionalInterface{
public void method(int x);
}
//람다식
MyFunctionalInterface fi = x -> {...};
//메소드 호출
fi.method(5);
@FunctionalInterface
public interface MyFunctionalInterface{
public int method(int x, int y);
}
//람다식
MyFunctionalInterface fi = (x, y) -> {...; return 값; };
//메소드 호출
fi.method(2,4);
람다식의 실행 블록에는 클래스의 멤버(필드와 메소드) 및 로컬 변수를 사용할 수 있다.
클래스의 멤버는 제약 사항 없이 사용 가능하지만, 로컬 변수는 제약이 따른다.
//함수적 인터페이스
@FunctionalInterface
public interface MyFunctionalInterface{
public void method();
}
//this 사용
public class UsingThis{
public int outter =10;
class Inner{
int inner =20;
void method(){
//람다식
MyFunctionalInterface fi = () -> {
System.out.println("outter: "+ outter);
System.out.println("outter: "+ UsingThis.this.outter+"\n");
//바깥 객체의 참조 얻기 위해서는 클래스명.this 사용
System.out.println("inner: "+ inner);
System.out.println("inner: "+ this.inner +"\n");//내부 객체는 this 사용 가능
};
fi.method;
}
}
}
//실행 클래스
public class UsingThisEx{
public static void main(String[]args){
UsingThis usingThis = new UsingThis();
UsingThis.Inner inner = new usingTHis.new Inner();
inner.method();
}
}
자바 8부터는 빈번하게 사용되는 함수적 인터페이스(functional interface)는 java.util.function 표준 API로 제공한다.
이 패키지에서 제공하는 함수적 인터페이스의 목적은 메소드 또는 생성자의 매개 타입으로 사용되어 람다식을 대입할 수 있도록 하기 위해서이다.
java.util.function 패키지의 함수적 인터페이스는 크게 Consumer, Supplier, Function, Operator, Predicate로 구분 되며, 구분 기준은 인터페이스에 선언된 추상 메소드의 매개값과 리턴값의 유무이다.
인터페이스명 | 추상 메소드 | 설명 |
---|---|---|
Consumer | void accept(T t) | 객체를 T를 받아 소비 |
BiConsumer<T,U> | void accept(T t, U u) | 객체 T, U를 받아 소비 |
DoubleConsumer | void accept(double value) | double 값을 받아 소비 |
intConsumer | void accept(int value) | int 값을 받아 소비 |
LongConsumer | void accept(long value) | long 값을 받아 소비 |
ObjDoubleConsumer | void accept(T t, double value) | 객체 T와 double 값을 받아 소비 |
ObjIntConsumer | void accept(T t, int value) | 객체 T와 int 값을 받아 소비 |
ObjLongConsumer | void accept(T t, long value) | 객체 T와 long 값을 받아 소비 |
//Consumer<T> 인터페이스를 타겟 타입으로 하는 람다식
Consumer<String> consumer = t -> {t를 소비하는 실행문;};
//ObjIntConsumer<T> 인터페이스를 타겟 타입으로 하는 람다식
ObjIntConsumer consumer = <t,i> -> {t와 i를 소비하는 실행문}
//Consumer 함수적 인터페이스
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.DoubleConsumer;
import java.util.function.ObjIntConsumer;
public class ConsumerEx {
public static void main(String[] args) {
Consumer<String> consumer = t-> System.out.println(t+"8");
consumer.accept("java");
BiConsumer<String, String> biConsumer = (t, u) -> System.out.println(t+u);
biConsumer.accept("java","8");
DoubleConsumer doubleConsumer = d -> System.out.println("java"+d);
doubleConsumer.accept(8.0);
ObjIntConsumer oiConsumer = (t, u) -> System.out.println(t+u);
oiConsumer.accept("java", 8);
}
}
Supplier 함수적 인터페이스는 매개 변수가 없고 리턴값이 있는 getXXX() 메소드를 가진다. 이 메소드들은 실행 후 호출한 곳으로 데이터를 리턴(공급)하는 역할을 한다.
리턴 타입에 따른 Supplier 함수적 인터페이스
인터페이스명 | 추상 메소드 | 설명 |
---|---|---|
Supplier | T get() | T 객체를 리턴 |
BooleanSupplier | boolean getAsBoolean() | boolean 값을 리턴 |
DoubleSupplier | double getAsDouble() | double 값을 리턴 |
IntSupplier | int getAsInt() | int 값을 리턴 |
LongSupplier | long getAsLong() | long 값을 리턴 |
예제 : 주사위 수 랜덤
import java.util.function.IntSupplier;
public class SupplierEx {
public static void main(String[] args) {
IntSupplier intSupplier = () -> {
int num = (int) (Math.random()* 6) + 1;
return num;
}; // 람다식
int num = intSupplier.getAsInt();
System.out.println("눈의 수: " +num);
}
}
인터페이스명 | 추상메서드 | 설명 |
---|---|---|
Function<T,R> | R apply(T t) | 객체 T를 객체 R로 매핑 |
BiFunction<T,U,R> | R apply(T t, U u) | 객체 T와 U를 객체 R로 매핑 |
DoubleFunction | R apply(double val) | double 를 객체 R로 매핑 |
IntFunction | R apply(int val) | int 를 객체 R로 매핑 |
IntToDoubleFunction | double applyAsdouble(int val) | int를 double로 매핑 |
IntToLongFunction | long applyAsLong(int val) | int를 long로 매핑 |
LongToDoubleFunction | double applyAsdouble(long val) | long을 double로 매핑 |
LongToIntFunction | int applyAsInt(long val) | long을 int로 매핑 |
ToDoubleBiFunction<T,U> | double applyAsDouble(T t, U u) | 객체 T와 U를 double 로 매핑 |
ToDoubleFunction | double applyAsdouble(T t) | 객체 T를 double로 매핑 |
ToIntBiFunction<T,U> | int applyAsInt(T t, U u) | 객체 T와 U를 int로 매핑 |
ToIntFunction | int applyAsInt(T t) | 객체 T를 int로 매핑 |
ToLongBiFunction<T,U> | long applyAsLong(T t, U u) | 객체 T와 U를 long으로 매핑 |
ToLongFunction | long applyAsLong(T t) | 객체 T를 long으로 매핑 |
//Student 클래스
public class Student {
private String name;
private int engScore;
private int korScore;
public Student(String name, int engScore, int korScore) {
this.name=name;
this.engScore=engScore;
this.korScore=korScore;
}
public String getName() {
return this.name;
}
public int getEngScore() {
return this.engScore;
}
public int getKorScore() {
return this.korScore;
}
}
//FunctionEx
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.function.ToIntFunction;
public class FunctionEx {
private static List <Student> list = Arrays.asList(
new Student("홍길동", 90,58),
new Student("신용권", 10,30)
);
public static void printString(Function<Student, String> function) {
for(Student student : list) {
System.out.print(function.apply(student)+" ");
}
System.out.println();
}
public static void printInt(ToIntFunction<Student> function) {
for(Student student : list) {
System.out.print(function.applyAsInt(student)+ " ");
}
System.out.println();
}
public static void main(String[]args) {
System.out.println("[학생 이름]");
printString(t-> t.getName());
System.out.println("[영어 점수]");
printInt(t-> t.getEngScore());
System.out.println("[국어 점수]");
printInt(t-> t.getKorScore());
}
}
인터페이스명 | 추상메서드 | 설명 |
---|---|---|
BinaryOperator | BiFunction<T,U,R>의 하위 인터페이스 | T와 U를 연산한 후 R 리턴 |
UnaryOperator | Function<T,R>의 하위 인터페이스 | T를 연산한 후 R 리턴 |
DoubleBinaryOperator | double applyAsDouble(double, double) | 두 개의 double 연산 |
DoubleUnaryOperator | double applyAsDouble(double) | 한 개의 double 연산 |
IntBinaryOperator | int applyAsInt(int,int) | 두 개의 int 연산 |
IntUnaryOperator | int applyAsInt(int) | 한 개의 int 연산 |
LongBinaryOperator | long applyAsLong(long, long) | 두 개의 long 연산 |
LongUnaryOperator | long applyAsLong(long) | 한 개의 long 연산 |
import java.util.function.IntBinaryOperator;
public class OperatorEx {
private static int[] scores = {92, 30,90};
public static int maxOrMin(IntBinaryOperator operator) {
int result = scores[0];
for(int score : scores) {
result = operator.applyAsInt(result, score);
}
return result;
}
public static void main(String[]args) {
//최대값 얻기
int max = maxOrMin(
(a,b) -> {
if(a>=b) return a;
else return b;
}
);
System.out.println("최대값: " + max);
//최소값 얻기
int min = maxOrMin(
(a,b) -> {
if(a<=b) return a;
else return b;
}
);
System.out.println("최소값: " + min);
}
}
인터페이스명 | 추상 메소드 | 설명 |
---|---|---|
Predicate | boolean test(T t) | 객체 T를 조사 |
BiPredicate<T, U> | boolean test(T t, U u) | 객체 T와 U를 비교 조사 |
DoublePredicate | boolean test(double value) | double 값을 조사 |
IntPredicate | boolean test(int value) | int 값을 조사 |
LongPredicate | boolean test(long value) | long 값을 조사 |
//Student2 클래스
public class Student2 {
private String name;
private String sex;
private int score;
public Student2(String name, String sex, int score) {
this.name=name;
this.sex=sex;
this.score=score;
}
public String getName() {
return this.name;
}
public String getSex() {
return this.sex;
}
public int getScore() {
return this.score;
}
}
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
public class PredicateEx {
private static List<Student2> list = Arrays.asList(
new Student2("홍길동", "남자", 90),
new Student2("김길동", "남자", 80),
new Student2("김자바", "여자", 97),
new Student2("박자바", "여자", 70)
);
public static double avg(Predicate<StudentP> predicate) {
int count=0, sum=0;
for(Student2 student : list) {
if(predicate.test(student)) {
count++;
sum += student.getScore();
}
}
return (double) sum/count;
}
public static void main(String[] args) {
double maleAvg = avg(t-> t.getSex().equals("남자"));
System.out.println("남자 평균 점수: "+ maleAvg);
double femaleAvg = avg(t-> t.getSex().equals("여자"));
System.out.println("여자 평균 점수: "+ femaleAvg);
}
}
디폴트 및 정적 메소드는 추상 메소드가 아니기 때문에 함수적 인터페이스에 선언되어도 여전히 함수적 인터페이스의 성질을 잃지 않는다.
Consumer, Function, Operator 종류의 함수적 인터페이스는 andThen()과 compose() 디폴트 메소드를 가지고 있다. 둘 다 두 개의 함수적인 인터페이스를 순차적으로 연결하고 , 첫 처리 결과를 두번째 매개값으로 제공해서 최종 결과값을 얻는다.
인터페이스AB = 인터페이스A.andThen(인터페이스B);
최종결과 = 인터페이스AB.method(); // andThen()은 A처리 ->A결과로 B처리
인터페이스AB = 인터페이스A.compose(인터페이스B);
최종결과 = 인터페이스AB.method(); // andThen()은 B처리 ->B결과로 A처리
andThen()과 compose() 디폴트 메소드
종류 | 함수적 인터페이스 | andThen() | compose() |
---|---|---|---|
Consumer | Consumer | O | |
BiConsumer<T, U> | O | ||
DoubleConsumer | O | ||
IntConsumer | O | ||
LongConsumer | O | ||
Function | Function<T, R> | O | O |
BiFunction<T, U, R> | O | ||
Operator | BinaryOperator | O | |
DoubleUnaryOperator | O | O | |
IntUnaryOperator | O | O | |
LongUnaryOperator | O | O |
import java.util.function.Consumer;
public class ConsumerAndThenEx {
public static void main(String[] args) {
Consumer<Member> consumerA = (m) -> {
System.out.println("consumerA : "+m.getName());
};
Consumer<Member> consumerB = (m) -> {
System.out.println("consumerB : "+m.getId());
};
Consumer<Member> consumerAB = consumerA.andThen(consumerB);
consumerAB.accept(new Member("홍길동", "hong", null) );
}
}
//Member 클래스
public class Member {
private String name;
private String id;
private Address address;
public Member(String name, String id, Address address) {
this.name=name;
this.id=id;
this.address=address;
}
public String getName() {
return name;
}
public String getId() {
return id;
}
public Address getAddress() {
return address;
}
}
//Address클래스
public class Address {
private String country;
private String city;
public Address(String country, String city) {
this.country=country;
this.city=city;
}
public String getCountry() {
return country;
}
public String getCity() {
return city;
}
}
import java.util.function.Function;
public class FuntionAndThenComposeEx {
public static void main(String[]args) {
Function<Member, Address> functionA;
Function<Address, String> functionB;
Function<Member, String> functionAB;
String city;
functionA = (m)->m.getAddress();
functionB = (a)->a.getCity();
functionAB = functionA.andThen(functionB);
city = functionAB.apply(
new Member("홍길동", "hong", new Address("한국", "서울"))
);
System.out.println("거주 도시 : "+city);
functionAB = functionB.compose(functionA);
city = functionAB.apply(
new Member("홍길동", "hong", new Address("한국", "서울"))
);
System.out.println("거주 도시: " +city);
}
}
import java.util.function.IntPredicate;
public class PredicteAndOrNegateEx {
public static void main(String[] args) {
// 2배수 검사
IntPredicate predicateA = a-> a%2 ==0;
// 3배수 검사
IntPredicate predicateB = b-> b%3 ==0;
IntPredicate predicateAB;
boolean result;
//and()
predicateAB = predicateA.and(predicateB);
result = predicateAB.test(9);
System.out.println("9는 2와 3의 배수입니까?" +result);
//or()
predicateAB = predicateA.or(predicateB);
result = predicateAB.test(9);
System.out.println("9는 2 또는 3의 배수입니까?" +result);
//negate()
predicateAB = predicateA.negate();// 원래 결과 true이면 false
result = predicateAB.test(9);
System.out.println("9는 홀수입니까?" +result);
}
}
Predicate<Object> predicate = Predicate.isEqual(targetObeject);
boolean result = predicate.test(sourceObject);//Objects.equals(sourceObject, targetObject) 실행
sourceObject | targetObject | 리턴값 |
---|---|---|
null | null | true |
not null | null | false |
null | not null | false |
not null | not null | Objects.equals(sourceObject, targetObject)의 리턴값 |
//예제
import java.util.function.Predicate;
public class PredicateIsEqualEx {
public static void main(String[] args) {
// TODO Auto-generated method stub
Predicate<String> predicate;
predicate = Predicate.isEqual("java8");
System.out.println("java8, null : " +predicate.test(null));
predicate = Predicate.isEqual(null);
System.out.println("null, null : " +predicate.test(null));
}
}
리턴 타입 | 정적 메소드 |
---|---|
BinaryOperator | minBy(Comparator<? super T> comparator) |
BinaryOperator | maxBy(Comparator<? super T> comparator) |
@FunctionalInterface
public interface Comparator<T>{
public int compare(T o1, T o2);
}
(o1, o2) -> {...; return int값;} //람다식
import java.util.function.BinaryOperator;
public class OperatorMinByMaxByEx {
public static void main(String[] args) {
BinaryOperator<Fruit> binaryOperator;
Fruit fruit;
binaryOperator = BinaryOperator.minBy((f1,f2)-> Integer.compare(f1.price,f2.price));
fruit= binaryOperator.apply(new Fruit("딸기", 1000), new Fruit("사과",2000));
System.out.println(fruit.name);
}
}
메소드 참조(Method Reference)는 메소드를 참조해서 매개 변수의 정보 및 리턴 타입을 알아내어, 람다식에서 불필요한 매개 변수를 제거하는 것이 목적이다.
메소드 참조도 람다식과 마찬가지로 인터페이스의 익명 구현 객체로 생성되므로 타겟 타입인 인터페이스의 추상 메소드가 어던 매개 변수를 가지고, 리턴 타입이 무엇인가에 따라 달라진다.
클래스 :: 메소드
참조변수 :: 메소드
public class MehodReferencesEx {
public static void main(String[] args) {
IntBinaryOperator operator;
//정적 메소드 참조
operator = Calculator :: staticMethod;
System.out.println("결과 : " + operator.applyAsInt(1, 2));
Calculator obj = new Calculator();
operator = obj :: instanceMethod;
System.out.println("결과 : " +operator.applyAsInt(2, 4));
}
}
(a,b) -> {a.instanceMethod(b);}
//클래스 :: instanceMethod
import java.util.function.ToIntBiFunction;
public class ArgumentMethodReferencesEx {
public static void main(String[] args) {
ToIntBiFunction<String,String> function;
function = (a,b) -> a.compareToIgnoreCase(b);
print(function.applyAsInt("java8", "JAVA8"));
function = String :: compareToIgnoreCase;
print(function.applyAsInt("java8", "JAVA8"));
}
public static void print(int order) {
if(order<0) {
System.out.println("사전순으로 먼저 옵니다.");
} else if(order == 0) {
System.out.println("동일한 문자열입니다.");
} else {
System.out.println("사전순으로 나중에 옵니다.");
}
}
}
(a,b) -> {return new 클래스(a,b);}
//클래스 :: new 로 대치 가능
import java.util.function.BiFunction;
import java.util.function.Function;
public class ConstructorReferenceEx {
public static void main(String[] args) {
Function<String, MemberC> function1 = MemberC :: new;
MemberC member1 = function1.apply("angel");
BiFunction<String,String, MemberC> function2 = MemberC :: new;
MemberC member2 = function2.apply("강강", "angel");
}
}
배열은 쉽게 생성하고 사용할 수 있지만, 저장할 수 있는 객체 수가 배열을 생성할 때 결정되기 때문에 붙특정 다수의 객체를 저장하기에는 문제가 있다. 자바는 이러한 문제점을 해결하고 널리 알려져 있는 자료구조를 바탕으로 객체들을 효율적으로 추가, 삭제, 검색할 수 있도록
java.utill
패키지에 컬렉션과 관려된 인터페이스와 클래스인 컬렉션 프레임워크(Collection Framework)을 포함시켜 놓았다.
인터페이스 분류 | 특징 | 구현클래스 |
---|---|---|
Collection (List) | - 순서를 유지하고 저장 - 중복 저장 가능 | ArrayList, Vector, LinkedList |
Collection (Set) | - 순서를 유지하지 않고 저장 - 중복 저장 안됨 | HashSet, TreeSet |
Map | - 키와 값의 쌍으로 저장 | HashMap, Hashable,TreeMap, Properties |
List 컬렉션은 객체를 일렬로 늘어놓은 구조를 가지고 있다. 객체를 인덱스로 관리하기 때문에 객체를 저장하면 자동 인덱스가 부여되고 인덱스로 객체를 검색, 삭제할 수 있는 기능을 제공한다.
List 컬렉션에는 ArrayList, Vector, LinkedList 등이 있다.
기능 | 메소드 | 설명 |
---|---|---|
객체 추가 | 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(0) | 저장되어 있는 전체 객체 수 반환 | |
객체 삭제 | void clear() | 저장된 모든 객체를 삭제 |
E remove(int index) | 주어진 인덱스에 저장된 객체를 삭제 | |
boolean remove(Object o) | 주어진 객체를 삭제 |
ArrayList는 List 인터페이스의 구현 클래스로, ArrayList에 객체를 추가하면 객체가 인덱스로 관리된다는 점은 배열과 비슷하지만, 배열은 생성할 때 크기가 고정적이고 사용 중에 변경 불가능하지만, ArrayList는 저장용량이 가변적으로 늘어난다.
ArrayLIst 생성 방법
`List<String> list = new ArrayList<String>;
기본 생성자로 생성 시 10개의 초기 용량(capacity)가 설정되어있다.
List<String> list = new ArrayList<String>(20)
매개값으로 초기 용량을 설정할 수 있다.
ArrayList에 객체를 추가하면 인덱스 0부터 차례대로 저장된다. 특정 인덱스의 객체를 제거하면 마지막 인덱스부터 앞으로 1칸씩 당겨진다. 반대로 삽입을 하면 1칸씩 밀려난다.
객체 삭제와 삽입이 많이 일어나는 로직에서는 ArrayList 사용이 맞지 않다.
예제
private static void method1() { //ArrayList
//List 인터페이스를 구현한 class: ArrayList
//순서가 있다. 중복을 허용한다.
List<String> list = new ArrayList<>(3);
String[] days = {"월요일", "화요일", "수요일", "목요일", "금요일", "토요일", "일요일"};
for(String s:days) {
list.add(s);
}
//함수연습
//get()
String s = list.get(6);
System.out.println("6번쨰 값: " + s);
//contains(), clear()
if(list.contains("화요일")) { //검색
System.out.println("화요일이 있습니다.");
}
for(int i = 0; i < list.size(); i++) {
System.out.println(i + "번째-->" + list.get(i));
}
}
Vector는 ArrayList와 동일한 내부 구조이다. Vector를 생성하기 위해서 저장할 객체타입을 타입 파라미터로 표기하고 기본 생성자를 호출하면 된다.
Vector 생성 방법
List<E> list = new Vector<E>();
ArrayList와 차이점은 동기화(synchronized) 메소드로 구성되어 있어 멀티 스레드가 동시에 실행할 수 없고 하나의 스레드가 실행을 완료해야만 다른 스레드를 실행할 수 있으므로, 멀티 스레드 환경에서 안전하게 객체를 추가, 삭제할 수 있다.
예제
private static void method3() { //Vector
//List 인터페이스를 구현한 class: Vector
//순서가 있다. 중복을 허용한다.
List<String> list = new Vector<>();
String[] days = {"월요일", "화요일", "수요일", "목요일", "토요일", "토요일", "일요일"};
for(String s:days) {
list.add(s);
}
for(String s:list) {
System.out.print(s + " ");
}
}
LinkedList는 List 구현 클래스로 ArrayList와 사용 방법은 똑같으나 내부 구조가 다르다. ArrayList는 저장된 객체를 인덱스로 관리하지만, LinkedList는 인접 참조를 링크하여 체인처럼 관리한다. 특정 인덱스의 객체를 제거하더라도 앞뒤 링크만 변경되고 나머지 링크는 변경되지 않는다. 객체 삽입과 삭제가 많이 일어나는 로직에서 ArrayList보다 좋은 성능을 보인다.
LinkedList 생성 방법
List<E> list = new LinkedList<E>();
예제
private static void method2() { //LinkedList
//List 인터페이스를 구현한 class: LinkedList
//순서가 있다. 중복을 허용한다. 체인처럼 관리
List<String> list = new LinkedList<>();
String[] days = {"월요일", "화요일", "수요일", "목요일", "토요일", "토요일", "일요일"};
for(String s:days) {
list.add(s);
}
for(String s:list) {
System.out.print(s + " ");
}
}
List 컬렉션은 저장 순서를 유지하지만, Set 컬렉션은 저장 순서가 유지되지 않는다. 객체 중복 저장이 불가능하며 null도 하나만 저장이 가능하다. 수학으로 비유하면 집합에 가깝다.
Set 컬렉션은 HashSet, LinkedHashSet, TreeSet 등이 있다. 인덱스로 관리하지 않기 떄문에 인덱스를 매개값으로 갖는 메소드가 없다.
기능 | 메소드 | 설명 |
---|---|---|
객체 추가 | boolean add(E e) | 주어진 객체를 저장, 객체가 저장되면 true 리턴하고 중복 객체면 false 리턴 |
객체 검색 | boolean contains(Object o) | 주어진 객체가 저장되어 있는지 여부 |
isEmpty() | 컬렉션이 비어 있는지 조사 | |
Iterator iterator() | 저장된 객체를 한 번씩 가져오는 반복자 리턴 | |
int size() | 저장되어있는 전체 객체 수 리턴 | |
객체 삭제 | void clear() | 저장된 모든 객체를 삭제 |
boolean remove(Object o) | 주어진 객체를 삭제 |
Set 컬렉션은 인덱스로 객체를 검색하는 메소드가 없기 때문에 전체 객체를 대상으로 한 번씩 가져오는 반복자(Iterator)를 제공한다. 반복자는 Iterator 인터페이스를 구현한 객체이다.
반복자 호출 방법
Set<String> Set = ...;
Iterator<String> iterator = set.iterator();
Iterator 인터페이스 메소드
리턴 타입 | 메소드 | 설명 |
---|---|---|
boolean | hasNext() | 가져올 객체가 있으면 true, 없다면 false 리턴 |
E | next() | 컬렉션에서 하나의 객체를 반환 |
void | remove() | Set 컬렉션에서 객체를 제거 |
예제
private static void method4() {
String[] days = {"월요일", "화요일", "수요일", "목요일", "토요일", "토요일", "일요일"};
//배열을 List로 변경
List<String> list = Arrays.asList(days);
print(list);
System.out.println();
System.out.println(list.contains("일요일") ? "있음" : "없음");
//3.Iterator 반복자 이용해서 읽기
Iterator<String> it = list.iterator();
while(it.hasNext()) {
String s = it.next();
System.out.print(s + " ");
}
System.out.println("모두 읽음");
}
Iterator를 사용하지 않아도 확장 for문을 활용하여 전체 객체를 반복할 수 있다.
for(String s:set) { System.out.print(s + " "); }
HashSet은 Set 인터페이스의 구현 클래스 중 하나로, 객체들을 순서 없이 저장하고 중복 저장하지 않는다. 객체를 저장하기 전에 먼저 객체의
hashCode()
메소드를 호출하여 해시코드를 얻어내 이미 저장되어 있는 객체들과 비교하여 중복 저장여부를 판단한다.
HashSet 생성 방법
Set<E> set = new HashSet<E>();
문자열을 HashSet에 저장할 경우, 같은 문자열을 갖는 String 객체는 동등한 객체로 간주된다. 그 이유는 String 객체가 가지고 있는 hashCode()
와 equals()
의 리턴값이 true
로 설정되어 있기 때문이다.
예제
private static void method7() {
Set<CustomerDTO> data = new HashSet<>();
CustomerDTO c1 = new CustomerDTO(100, "홍길동", "010-1234-5678", "서울");
CustomerDTO c2 = new CustomerDTO(100, "홍길동", "010-4342-7812", "광주");
System.out.println(c1.hashCode());
System.out.println(c2.hashCode());
System.out.println(c1.equals(c2));
data.add(c1);
data.add(c2);
data.add(new CustomerDTO(300, "박길동", "010-4434-5645", "울산"));
data.add(new CustomerDTO(400, "최길동", "010-2234-5741", "경기"));
data.add(new CustomerDTO(200, "홍길동", "010-1234-5678", "서울"));
//Set은 중복을 허용하지 않는데 위에 마지막 홍길동과 첫 번째 홍길동은 중복이 허용되버림
//주소값이 다르기 때문
//그래서 DTO에 중복체크 코드를 추가 (equals(), hashCode() Override ㄱ ㄱ)
System.out.println(data);
for(CustomerDTO c : data) {
System.out.println(c);
}
}
private static void method6() {
Set<String> set = new HashSet<>();
set.add("월요일");
set.add("화요일");
set.add("수요일");
set.add("토요일");
set.add("토요일");
set.add("일요일");
System.out.println(set.size() + "개"); //중복을 허용하지 않는다.
System.out.println(set); //toString() override
}
private static void method5() { //Set
//Set interface를 구현한 class: HashSet, TreeSet, LinkedHashSet
//순서가 없다. 중복이 불가하다. 키로 사용된다.
Set<String> set = new HashSet<>();
set.add("월요일");
set.add("화요일");
set.add("토요일");
set.add("토요일");
set.add("일요일");
System.out.println(set.size() + "개"); //중복을 허용하지 않는다.
//1.일반 for로 읽기 ... 불가
//2.확장 for로 읽기 ... 가능 (순서는 없음)
for(String s:set) { System.out.print(s + " "); }
System.out.println();
//3.Iterator로 읽기(반복자)
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
iterator.next();
System.out.print(iterator.next() + " "); //읽을게 없으면 NoSuchElementException 발생
}
}
Map 컬렉션은 키(Key)와 값(Value)로 구성된 Entry 객체를 저장하는 구조이다. 키와 값 모두 객체이며, 키는 중복될 수 없지만 값은 중복 저장될 수 있다. 만약 기존에 저장된 키와 동일한 키로 값을 저장하면 새로운 값으로 대치된다.
Map 컬렉션은 HashMap, HashTable, LinkedHashMap, Propertiess, TreeMap 등이 있다.
기능 | 메소드 | 설명 |
---|---|---|
객체 추가 | V put(K key, V value) | 주어진 키와 값을 추가, 저장이 되면 값을 리턴 |
객체 검색 | boolean containsKey(Object key) | 주어진 키가 있는지 여부 |
boolean containsValue(Object value) | 주어진 값이 있는지 여부 | |
Ser<Map.Entry<K, V>> entrySet() | 키와 값의 쌍으로 구성된 모든 Map.Entry 객체를 Set에 담아서 리턴 | |
V get(Object key | 주어진 키의 값을 리턴 | |
boolean isEmpty() | 컬렉션이 비어있는지 여부 | |
Set keySet() | 모든 키를 Set 객체에 담아서 리턴 | |
int size() | 저장된 키의 총 수를 리턴 | |
Collection values() | 저장된 모든 값 Collection에 담아서 리턴 | |
객체 삭제 | void clear() | 모든 Map.Entry(키와 값)를 삭제 |
V remove(Object key) | 주어진 키와 일치하는 Map.Entry를 삭제, 삭제가 되면 값을 리턴 |
HashMap은 Map 인터페이스를 구현한 대표적인 Map 컬렉션으로 HashMap의 키로 사용할 객체는
hashCode()
와equals()
메소드를 재정의해서 동등 객체가 될 조건을 정해야한다.주로 키 타입은 String을 많이 사용하는데 String은 문자열이 같을 경우 동등 객체가 될 수 있도록
hashCode()
와equals()
메소드가 재정의되어 있기 때문이다.
HashMap 생성 방법
Map<String, Integer> map = new HashMap<String, Integer>();
키와 값의 타입은 기본 타입을 사용할 수 없고 클래스 및 인터페이스 타입만 가능하다.
예제
private static void method2() {
Map<String, Integer> map = new HashMap<>();
map.put("홍길동", 100);
map.put("홍길동", 90);
map.put("박길동", 100);
map.put("최길동", 100);
map.put("고길동", 100);
//키가있는지?
System.out.println(map.containsKey("박길동2"));
//값이있는지?
System.out.println(map.containsValue(90));
Collection<Integer> scores = map.values();
int sum = 0;
for(Integer score : scores) {
sum+=score;
}
System.out.println("총점 : " + sum);
}
private static void method1() { //HashMap
//Map interface 구현 class: HashMap, TreeMap, Properties ...
//키와 값의 쌍 (Map.Entry)
//키는 중복 저장될 수 없지만 값은 중복 저장 가능
//만약 기존에 저장된 키와 동일한 키로 값을 저장하면 기존값 삭제 후 새로운 값 대체 (덮어쓰기)
Map<String, Integer> map = new HashMap<>();
map.put("홍길동", 100);
map.put("홍길동", 90);
map.put("박길동", 100);
map.put("최길동", 100);
map.put("고길동", 100);
System.out.println(map);
//키를 이용하여 값을 획득
int value = map.get("홍길동");
System.out.println("value=" + value);
//모든키를 얻기
Set<String> keys = map.keySet();
for (String key : keys) {
System.out.println(key + "-->" + map.get(key));
}
//Entry 얻기
//Iterator 사용
Set<Map.Entry<String, Integer>> entrys = map.entrySet();
Iterator<Map.Entry<String, Integer>> iterator = entrys.iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
System.out.println("---------");
System.out.println("키: " + entry.getKey());
System.out.println("값: " + entry.getValue());
}
System.out.println("=============================");
//확장 for문 사용
for(Map.Entry<String, Integer> entry:map.entrySet()) {
System.out.println("---------");
System.out.println("키: " + entry.getKey());
System.out.println("값: " + entry.getValue());
}
}
HashTable은 HashMap과 동일한 내부 구조이다. 차이점은 동기화(sychronized) 메소드로 구성되어 있기 때문에 멀티 스레드 환경에서 유리하다는 점이다.
HashTable 생성 방법
Map<String, Integer> map = new HashTable<String, Integer<();
예제
private static void method3() { //HashTable
Map<Student2, CustomerDTO> map = new Hashtable<>();
map.put(new Student2("학생1", "남", 100),
new CustomerDTO(1, "고객1", "02-1234-5678", "Seoul"));
map.put(new Student2("학생2", "남", 100),
new CustomerDTO(2, "고객2", "02-1234-5678", "Seoul"));
map.put(new Student2("학생3", "남", 100),
new CustomerDTO(3, "고객3", "02-1234-5678", "Seoul"));
//제거하기 (전체)
//map.clear();
//제거하기 (1개)
CustomerDTO cust = map.remove(new Student2("학생3", "남", 100));
System.out.println(cust + " 삭제");
//출력
for(Map.Entry<Student2, CustomerDTO> entry:map.entrySet()) {
System.out.println("key: " + entry.getKey());
System.out.println("value: " + entry.getValue());
System.out.println("==========================");
}
}
Properties는 HashTable 하위 클래스로 HashTable의 모든 특징을 갖고 있다. 차이점은 HashTable은 키와 값을 다양한 타입으로 지정이 가능한데 비해 Properties는 키와 값을 String 타입으로 제한한 컬렉션이다.
Properties는 애플리케이션의 옵션 정보, 데이터베이스 연결 정보, 다국어 정보가 저장된 프로퍼티(~.properties) 파일을 읽을 떄 주로 사용한다.
Properties 생성 방법
Properties properties = new Properties();
properties.load(new FileReader("~/경로"));
예제
package com.kosta.day14.chapter15_Collection;
import java.io.FileReader;
import java.io.IOException;
import java.util.Map.Entry;
import java.util.Properties;
public class PropertiesExample {
public static void main(String[] args) throws IOException {
method1();
}
private static void method1() throws IOException {
Properties pro = new Properties();
String fsrc = "src/com/kosta/day14/chapter15_Collection/oracleInfo.properties";
pro.load(new FileReader(fsrc));
for(Entry entry:pro.entrySet()) {
System.out.println("key: " + entry.getKey());
System.out.println("value: " + entry.getValue());
System.out.println("==========================");
}
}
}
컬렉션 프레임워크는 검색 기능을 강화시킨 TreeSet과 TreeMap이 제공된다. 각각 Set과 Map 컬렉션으로 이 컬렉션들은 이진 트리를 이용하여 계층적 구조(트리 구조)를 가진다.
TreeSet은 이진트리 기반으로한 Set 컬렉션으로 하나의 노드는 노드값인 value와 왼쪽과 오른쪽 자식 노드를 참조하기 위한 두 개의 변수로 구성되어있다. TreeSet에 객체를 저장하면 자동으로 정렬되고 부모값과 비교하여 낮은 것은 왼쪽 자식 노드, 높은 것은 오른쪽 자식 노드에 저장된다.
주요 메소드
리턴 타입 | 메소드 | 설명 |
---|---|---|
E | first() | 제일 낮은 객체 리턴 |
E | last() | 제일 높은 객체 리턴 |
E | lower(E e) | 주어진 객체보다 바로 아래 객체 리턴 |
E | higher(E e) | 주어진 객체보다 바로 위 객체를 리턴 |
E | floor(E e) | 주어진 객체와 같으면 리턴, 없다면 바로 아래 객체 리턴 |
E | ceiling(E e) | 주어진 객체와 같으면 리턴, 없다면 바로 위 객체 리턴 |
E | pollFirst() | 제일 낮은 객체를 꺼내오고 컬렉션에서 제거 |
E | pollLast(0) | 제일 높은 객체를 꺼내오고 컬렉션에서 제거 |
TreeSet 생성 방법
TreeSet<E> treeSet = new TreeSet<E>();
String 객체를 저장하는 TreeSet 선언 방법은
TreeSet<String> treeSet = new TreeSet<String>();
예제
package com.kosta.day14.chapter15_Collection;
import com.kosta.day13.chapter15_Collection.model.CustomerDTO;
import java.util.NavigableSet;
import java.util.Set;
import java.util.TreeSet;
public class CollectionTest_TreeSet {
public static void main(String[] args) {
method5();
}
private static void method5() { //정렬 바꾸기
TreeSet<CustomerDTO> data = new TreeSet<>();
data.add(new CustomerDTO(1, "고객1", "010-111-1111", "서울"));
data.add(new CustomerDTO(2, "고객2", "010-121-1111", "부산"));
data.add(new CustomerDTO(3, "고객3", "010-131-1111", "서울"));
data.add(new CustomerDTO(4, "고객4", "010-141-1111", "부산"));
data.add(new CustomerDTO(4, "고객4", "010-141-1111", "서울"));
NavigableSet<CustomerDTO> descendingSet = data.descendingSet();
for(CustomerDTO cust:descendingSet) {
System.out.println(cust);
}
NavigableSet<CustomerDTO> ascendingSet = descendingSet.descendingSet();
for(CustomerDTO cust:ascendingSet) {
System.out.println(cust);
}
}
private static void method4() {
TreeSet<CustomerDTO> data = new TreeSet<>();
data.add(new CustomerDTO(1, "고객1", "010-111-1111", "서울"));
data.add(new CustomerDTO(2, "고객2", "010-121-1111", "부산"));
data.add(new CustomerDTO(3, "고객3", "010-131-1111", "서울"));
data.add(new CustomerDTO(4, "고객4", "010-141-1111", "부산"));
data.add(new CustomerDTO(4, "고객4", "010-141-1111", "서울"));
//pollFirst: 제일 낮은 객체를 가져오고 컬렉션 제거
while(!data.isEmpty()) {
CustomerDTO cust = data.pollFirst();
System.out.println(cust);
System.out.println(data.size() + "명");
}
//pollLast: 제일 높은 객체를 가져오고 컬렉션 제거 //위에서 다 제거했으므로 의미없음
while(!data.isEmpty()) {
CustomerDTO cust = data.pollLast();
System.out.println(cust);
System.out.println(data.size() + "명");
}
}
private static void method3() {
TreeSet<CustomerDTO> data = new TreeSet<>();
data.add(new CustomerDTO(1, "고객1", "010-111-1111", "서울"));
data.add(new CustomerDTO(2, "고객2", "010-121-1111", "부산"));
data.add(new CustomerDTO(3, "고객3", "010-131-1111", "서울"));
data.add(new CustomerDTO(4, "고객4", "010-141-1111", "부산"));
data.add(new CustomerDTO(4, "고객4", "010-141-1111", "서울"));
//첫 번째 노드 구하기
System.out.println("첫 번째 노드: " + data.first());
//마지막 노드 구하기
System.out.println("마지막 노드: " + data.last());
for(CustomerDTO cust:data) {
System.out.println(cust);
}
}
private static void method2() {
Set<String> data = new TreeSet<>();
data.add("월");
data.add("화");
data.add("수");
data.add("목");
data.add("목"); //중복허용x
for(String s:data) {
System.out.println(s);
}
}
private static void method1() { //TreeSet
//TreeSet은 값을 넣을때 이진노드(binary tree)이용, 크기를 비교해서 크면 오른쪽 노드, 작으면 왼쪽 노드에 저장
TreeSet<Integer> data = new TreeSet<>();
data.add(100);
data.add(50);
data.add(70);
data.add(30);
data.add(30); //중복허용x
//first: 첫 번째 노드 구하기
System.out.println("첫 번째 노드: " + data.first());
//last: 마지막 노드 구하기
System.out.println("마지막 노드: " + data.last());
//lower: 기준보다 바로 밑 노드
System.out.println("기준보다 바로 아래의 노드: " + data.lower(50));
//higher: 기준보다 바로 위 노드
System.out.println("기준보다 바로 위의 노드: " + data.higher(new Integer(50)));
//floor: 주어진 객체와 동등한 객체가 있으면 리턴, 없으면 바로 아래 리턴턴
System.out.println("기준보다 같거나 아래의 노드: " + data.floor(50));
//ceiling: 주어진 객체와 동등한 객체가 있으면 리턴, 없으면 바로 위 리
System.out.println("기준보다 같거나 위의 노드: " + data.ceiling(50));
for(Integer i:data) {
System.out.println(i);
}
}
}
TreeMap은 이진 트리를 기반으로 한 Map 컬렉션으로 TreeSet과 차이점은 키와 값이 저장된 Map.Entry를 저장한다는 점이다. TreeMap에 객체를 저장하면 자동으로 정렬되고 부모 키값과 비교하여 낮은 것은 왼쪽 자식 노드, 높은 것은 오른쪽 자식 노드에 Map.Entry 객체를 저장한다.
주요 메소드
리턴 타입 | 메소드 | 설명 |
---|---|---|
Map.Entry<K, V> | firstEntry() | 제일 낮은 Map.Entry 리턴 |
Map.Entry<K, V> | lastEntry() | 제일 높은 Map.Entry 리턴 |
Map.Entry<K, V> | lowerEntry() | 주어진 키보다 바로 아래 Map.Entry 리턴 |
Map.Entry<K, V> | higherEntry() | 주어진 키보다 바로 위 Map.Entry 리턴 |
Map.Entry<K, V> | floorEntry() | 키와 같은 키가 있으면 Map.Entry 리턴, 없다면 바로 아래의 Map.Entry 리턴 |
Map.Entry<K, V> | ceilingEntry() | 키와 같은 키가 있으면 Map.Entry 리턴, 없다면 바로 위의 Map.Entry 리턴 |
Map.Entry<K, V> | pollFirstEntry() | 제일 낮은 Map.Entry를 꺼내오고 컬렉션에서 제거 |
Map.Entry<K, V> | pollLastEntry() | 제일 높은 Map.Entry를 꺼내오고 컬렉션에서 제거 |
TreeMap 생성 방법
TreeMap<String, Integer> treeMap = new TreeMap<String, Integer>();
Map 인터페이스 타입 변수에 대입해도 되지만 TreeMap 클래스 타입에 대입한 이유는 특정 객체를 찾거나 범위 검색과 관련된 메소드를 사용하기 위해서다.
예제
package com.kosta.day14.chapter15_Collection;
import com.kosta.day13.chapter15_Collection.model.CustomerDTO;
import com.sun.javaws.IconUtil;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.TreeMap;
public class CollectionTest_TreeMap {
public static void main(String[] args) {
method2();
}
private static void method2() {
//HashMap: hashCode(), equals()를 구현해야 동작함
//TreeMap: Comparable.compareTo()를 구현해야 동작
TreeMap<Car, CustomerDTO> data = new TreeMap<>();
data.put(new Car("ABC", 1000),
new CustomerDTO(1, "고객1", "010-1234-5678", "서울"));
data.put(new Car("DDD", 5000),
new CustomerDTO(2, "고객2", "010-1235-5572", "부산"));
data.put(new Car("EEE", 2000),
new CustomerDTO(3, "고객3", "010-4574-3412", "가산"));
data.put(new Car("ZZZ", 7000),
new CustomerDTO(4, "고객4", "010-3222-8976", "나주"));
{
Entry<Car, CustomerDTO> entry = data.firstEntry();
System.out.println("key: " + entry.getKey());
System.out.println("Value: " + entry.getValue());
System.out.println("==============================================");
entry = data.lowerEntry(new Car("EEE모델", 2000));
System.out.println("lower: " + entry);
System.out.println("==============================================");
}
for(Entry<Car, CustomerDTO> entry:data.entrySet()) {
System.out.println("key: " + entry.getKey());
System.out.println("Value: " + entry.getValue());
System.out.println("----------------------------------------------");
}
NavigableMap<Car, CustomerDTO> desc = data.descendingMap();
for(Entry<Car, CustomerDTO> entry:desc.entrySet()) {
System.out.println("key: " + entry.getKey());
System.out.println("Value: " + entry.getValue());
System.out.println("----------------------------------------------");
}
}
private static void method1() {
Map<String, Integer> data = new TreeMap<>();
data.put("홍길동", 100);
data.put("고길동", 90);
data.put("김길동", 80);
data.put("최길동", 70);
data.put("박길동", 60);
for(Map.Entry<String, Integer> entry:data.entrySet()) {
System.out.println("key: " + entry.getKey());
System.out.println("value: " + entry.getValue());
System.out.println("==========================");
}
}
}