JavaRush /Java Blog /Random-KO /자바 8 가이드. 1부.
ramhead
레벨 13

자바 8 가이드. 1부.

Random-KO 그룹에 게시되었습니다

"Java는 아직 살아 있고 사람들은 이를 이해하기 시작했습니다."

Java 8 소개에 오신 것을 환영합니다. 이 가이드는 언어의 모든 새로운 기능을 단계별로 안내합니다. 짧고 간단한 코드 예제를 통해 인터페이스 기본 메서드 , 람다 표현식 , 참조 메서드반복 가능한 주석을 사용하는 방법을 알아봅니다 . 기사가 끝나면 스트림, 함수 인터페이스, 연결 확장 및 새로운 날짜 API와 같은 API의 최신 변경 사항에 익숙해질 것입니다. 지루한 텍스트로 가득한 벽은 없습니다. 주석이 달린 코드 조각만 잔뜩 있을 뿐입니다. 즐기다!

인터페이스의 기본 메소드

Java 8에서는 default 키워드를 사용하여 인터페이스에 구현된 비추상 메소드를 추가할 수 있습니다 . 이 기능은 확장 메서드 라고도 합니다 . 첫 번째 예는 다음과 같습니다. 추상 메서드 인 generate interface Formula { double calculate(int a); default double sqrt(int a) { return Math.sqrt(a); } } 외에도 Formula 인터페이스는 기본 메서드인 sqrt도 정의 합니다 . Formula 인터페이스를 구현하는 클래스는 추상적인 계산 메소드만 구현합니다 . 기본 sqrt 방법은 즉시 사용할 수 있습니다. 수식 개체는 익명 개체로 구현됩니다. 코드는 매우 인상적입니다. 6줄의 코드로 간단히 sqrt(a * 100) 을 계산할 수 있습니다 . 다음 섹션에서 살펴보겠지만 Java 8에는 단일 메소드 객체를 구현하는 더 매력적인 방법이 있습니다. Formula formula = new Formula() { @Override public double calculate(int a) { return sqrt(a * 100); } }; formula.calculate(100); // 100.0 formula.sqrt(16); // 4.0

람다 표현식

초기 버전의 Java에서 문자열 배열을 정렬하는 방법에 대한 간단한 예부터 시작하겠습니다. 통계 도우미 메서드 Collections.sort는 목록과 비교기를 사용하여 주어진 목록의 요소를 정렬합니다 . 흔히 일어나는 일은 익명 비교자를 생성하여 정렬 메서드에 전달하는 것입니다. 항상 익명 객체를 생성하는 대신 Java 8에서는 훨씬 적은 수의 구문, 람다 표현식을 사용할 수 있는 기능을 제공합니다 . 보시다시피 코드가 훨씬 짧고 읽기 쉽습니다. 하지만 여기서는 훨씬 더 짧아집니다. 한 줄짜리 메서드의 경우 {} 중괄호와 return 키워드를 제거할 수 있습니다 . 그러나 코드가 더욱 짧아지는 부분은 다음과 같습니다. Java 컴파일러는 매개변수 유형을 인식하므로 매개변수도 생략할 수 있습니다. 이제 람다 표현식이 실제 생활에서 어떻게 사용될 수 있는지 더 자세히 살펴보겠습니다. List names = Arrays.asList("peter", "anna", "mike", "xenia"); Collections.sort(names, new Comparator () { @Override public int compare(String a, String b) { return b.compareTo(a); } }); Collections.sort(names, (String a, String b) -> { return b.compareTo(a); }); Collections.sort(names, (String a, String b) -> b.compareTo(a)); Collections.sort(names, (a, b) -> b.compareTo(a));

기능적 인터페이스

람다 표현식은 Java 유형 시스템에 어떻게 적합합니까? 각 람다는 인터페이스에 의해 정의된 특정 유형에 해당합니다. 그리고 소위 기능적 인터페이스에는 선언된 추상 메서드가 정확히 하나만 포함되어야 합니다. 주어진 유형의 모든 람다 표현식은 이 추상 메소드에 해당합니다. 기본 메소드는 추상 메소드가 아니므로 기능적 인터페이스에 기본 메소드를 자유롭게 추가할 수 있습니다. 인터페이스에 추상 메서드가 하나만 포함되어 있는 경우 임의의 인터페이스를 람다 식으로 사용할 수 있습니다. 인터페이스가 이러한 조건을 충족하는지 확인하려면 @FunctionalInterface 주석을 추가해야 합니다 . 컴파일러는 이 주석을 통해 인터페이스에 하나의 메서드만 포함해야 한다는 알림을 받게 되며, 이 인터페이스에서 두 번째 추상 메서드를 발견하면 오류가 발생합니다. 예: @FunctionalInterface 주석이 선언되지 않은 경우에도 이 코드는 유효하다는 점을 명심하세요 . @FunctionalInterface interface Converter { T convert(F from); } Converter converter = (from) -> Integer.valueOf(from); Integer converted = converter.convert("123"); System.out.println(converted); // 123

메서드 및 생성자에 대한 참조

위의 예는 통계적 메소드 참조를 사용하여 더욱 단순화할 수 있습니다. Java 8에서는 :: 키워드 기호를 사용하여 메소드 및 생성자에 대한 참조를 전달할 수 있습니다 . 위의 예는 통계적 방법을 사용하는 방법을 보여줍니다. 하지만 객체에 대한 메서드를 참조할 수도 있습니다. 생성자에 대해 :: 사용이 어떻게 작동하는지 살펴보겠습니다 . 먼저 다양한 생성자를 사용하여 예제를 정의해 보겠습니다. 다음으로 새 사람 개체를 생성하기 위한 PersonFactory 팩토리 인터페이스를 정의합니다 . Converter converter = Integer::valueOf; Integer converted = converter.convert("123"); System.out.println(converted); // 123 class Something { String startsWith(String s) { return String.valueOf(s.charAt(0)); } } Something something = new Something(); Converter converter = something::startsWith; String converted = converter.convert("Java"); System.out.println(converted); // "J" class Person { String firstName; String lastName; Person() {} Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } } interface PersonFactory

{ P create(String firstName, String lastName); } 팩토리를 수동으로 구현하는 대신 생성자 참조를 사용하여 모든 것을 하나로 묶습니다. Person::new 를 통해 Person 클래스 의 생성자에 대한 참조를 만듭니다 . Java 컴파일러는 생성자의 서명을 PersonFactory.create 메소드 의 서명과 비교하여 자동으로 적절한 생성자를 호출합니다 . PersonFactory personFactory = Person::new; Person person = personFactory.create("Peter", "Parker");

람다 지역

람다 식에서 외부 범위 변수에 대한 액세스를 구성하는 것은 익명 개체에서 액세스하는 것과 비슷합니다. 인스턴스 필드 및 집계 변수는 물론 로컬 범위에서 최종 변수 에 액세스할 수 있습니다.
지역 변수에 접근하기
람다 식의 범위에서 final 수정자를 사용하여 지역 변수를 읽을 수 있습니다 . 그러나 익명 개체와 달리 변수는 람다 식에서 액세스하기 위해 final 로 선언할 필요가 없습니다 . 이 코드도 정확합니다. 그러나 num 변수는 변경 불가능한 상태로 유지되어야 합니다. 코드 컴파일을 위해 암시적 final이 되어야 합니다 . 다음 코드는 컴파일되지 않습니다. 람다 식 내에서 num을 변경하는 것도 허용되지 않습니다. final int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); num = 3;
인스턴스 필드 및 통계 변수에 액세스
지역 변수와 달리 람다 표현식 내에서 인스턴스 필드와 통계 변수를 읽고 수정할 수 있습니다. 우리는 익명 개체를 통해 이러한 동작을 알고 있습니다. class Lambda4 { static int outerStaticNum; int outerNum; void testScopes() { Converter stringConverter1 = (from) -> { outerNum = 23; return String.valueOf(from); }; Converter stringConverter2 = (from) -> { outerStaticNum = 72; return String.valueOf(from); }; } }
인터페이스의 기본 메소드에 대한 액세스
첫 번째 섹션의 수식 인스턴스 예제를 기억하시나요 ? Formula 인터페이스는 익명 객체를 포함하여 Formula 의 모든 인스턴스에서 액세스할 수 있는 기본 sqrt 메소드를 정의합니다. 람다 식에서는 작동하지 않습니다. 기본 메서드는 람다 식 내에서 액세스할 수 없습니다. 다음 코드는 컴파일되지 않습니다. Formula formula = (a) -> sqrt( a * 100);

내장된 기능 인터페이스

JDK 1.8 API에는 많은 내장 기능 인터페이스가 포함되어 있습니다. 그 중 일부는 이전 버전의 Java에서 잘 알려져 있습니다. 예를 들어 Comparator 또는 Runnable 입니다 . 이러한 인터페이스는 @FunctionalInterface 주석을 사용하여 람다 지원을 포함하도록 확장되었습니다 . 그러나 Java 8 API에는 여러분의 삶을 더 쉽게 만들어 줄 새로운 기능적 인터페이스도 가득합니다. 이러한 인터페이스 중 일부는 Google의 Guava 라이브러리 에서 잘 알려져 있습니다 . 이 라이브러리에 익숙하더라도 몇 가지 유용한 확장 방법을 사용하여 이러한 인터페이스가 어떻게 확장되는지 자세히 살펴봐야 합니다.
술어
조건자는 인수가 하나인 부울 함수입니다. 인터페이스에는 조건자를 사용하여 복잡한 논리식(및, 또는 부정)을 생성하기 위한 다양한 기본 방법이 포함되어 있습니다. Predicate predicate = (s) -> s.length() > 0; predicate.test("foo"); // true predicate.negate().test("foo"); // false Predicate nonNull = Objects::nonNull; Predicate isNull = Objects::isNull; Predicate isEmpty = String::isEmpty; Predicate isNotEmpty = isEmpty.negate();
기능
함수는 하나의 인수를 취하고 결과를 생성합니다. 기본 메소드를 사용하여 여러 기능을 하나의 체인(compose, andThen)으로 결합할 수 있습니다. Function toInteger = Integer::valueOf; Function backToString = toInteger.andThen(String::valueOf); backToString.apply("123"); // "123"
공급자
공급자는 한 가지 유형 또는 다른 유형의 결과(인스턴스)를 반환합니다. 함수와 달리 공급자는 인수를 사용하지 않습니다. Supplier personSupplier = Person::new; personSupplier.get(); // new Person
소비자
소비자는 단일 인수를 사용하여 인터페이스 메서드를 나타냅니다. Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Luke", "Skywalker"));
비교기
비교기는 이전 버전의 Java에서 알려져 있습니다. Java 8에서는 인터페이스에 다양한 기본 메소드를 추가할 수 있습니다. Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0
선택사항
Optionals 인터페이스는 작동하지 않지만 NullPointerException을 방지하는 데 유용한 유틸리티입니다 . 이는 다음 섹션에서 중요한 사항이므로 이 인터페이스가 어떻게 작동하는지 간단히 살펴보겠습니다. Optional 인터페이스는 null 이거나 null이 아닐 수 있는 값에 대한 간단한 컨테이너입니다. 메서드가 값을 반환하거나 아무것도 반환하지 않을 수 있다고 상상해 보세요. Java 8에서는 null을 반환하는 대신 Optional 인스턴스를 반환합니다 . Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0

개울

java.util.Stream은 하나 이상의 작업이 수행되는 일련의 요소입니다. 각 스트림 작업은 중간이거나 터미널입니다. 터미널 작업은 특정 유형의 결과를 반환하는 반면 중간 작업은 스트림 개체 자체를 반환하여 메서드 호출 체인을 생성할 수 있습니다. Stream은 목록 및 세트에 대한 java.util.Collection 과 같은 인터페이스입니다 (맵은 지원되지 않음). 각 Stream 작업은 순차적으로 또는 병렬로 실행될 수 있습니다. 스트림이 어떻게 작동하는지 살펴보겠습니다. 먼저 문자열 목록 형식으로 샘플 코드를 생성합니다. Java 8의 컬렉션은 Collection.stream() 또는 Collection.parallelStream() 을 호출하여 매우 간단하게 스트림을 생성할 수 있도록 향상되었습니다 . 다음 섹션에서는 가장 중요하고 간단한 스트림 작업을 설명합니다. List stringCollection = new ArrayList<>(); stringCollection.add("ddd2"); stringCollection.add("aaa2"); stringCollection.add("bbb1"); stringCollection.add("aaa1"); stringCollection.add("bbb3"); stringCollection.add("ccc"); stringCollection.add("bbb2"); stringCollection.add("ddd1");
필터
필터는 스트림의 모든 요소를 ​​필터링하기 위해 조건자를 허용합니다. 이 작업은 중간 작업이므로 결과(필터링된) 결과에 대해 다른 스트림 작업(예: forEach)을 호출할 수 있습니다. ForEach는 이미 필터링된 스트림의 각 요소에 대해 수행될 작업을 허용합니다. ForEach는 터미널 작업입니다. 또한 다른 작업을 호출하는 것은 불가능합니다. stringCollection .stream() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa2", "aaa1"
정렬됨
Sorted는 스트림의 정렬된 표현을 반환하는 중간 작업입니다. Comparator 를 지정하지 않으면 요소가 올바른 순서로 정렬됩니다 . stringCollection .stream() .sorted() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa1", "aaa2" sorted는 컬렉션 자체에 영향을 주지 않고 스트림의 정렬된 표현을 생성한다는 점을 명심하세요. stringCollection 요소 의 순서는 그대로 유지됩니다. System.out.println(stringCollection); // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
지도
중간 맵 작업은 결과 함수를 사용하여 각 요소를 다른 객체로 변환합니다. 다음 예에서는 각 문자열을 대문자 문자열로 변환합니다. 그러나 맵을 사용하여 각 객체를 다른 유형으로 변환할 수도 있습니다. 결과 스트림 객체의 유형은 맵에 전달하는 함수 유형에 따라 다릅니다. stringCollection .stream() .map(String::toUpperCase) .sorted((a, b) -> b.compareTo(a)) .forEach(System.out::println); // "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
성냥
스트림 관계에서 특정 술어의 진실성을 테스트하기 위해 다양한 일치 작업을 사용할 수 있습니다. 모든 일치 작업은 터미널이며 부울 결과를 반환합니다. boolean anyStartsWithA = stringCollection .stream() .anyMatch((s) -> s.startsWith("a")); System.out.println(anyStartsWithA); // true boolean allStartsWithA = stringCollection .stream() .allMatch((s) -> s.startsWith("a")); System.out.println(allStartsWithA); // false boolean noneStartsWithZ = stringCollection .stream() .noneMatch((s) -> s.startsWith("z")); System.out.println(noneStartsWithZ); // true
세다
Count는 스트림의 요소 수를 long 으로 반환하는 터미널 작업입니다 . long startsWithB = stringCollection .stream() .filter((s) -> s.startsWith("b")) .count(); System.out.println(startsWithB); // 3
줄이다
전달된 함수를 사용하여 스트림의 요소를 단축하는 터미널 작업입니다. 결과는 단축된 값을 포함하는 Optional이 됩니다 . Optional reduced = stringCollection .stream() .sorted() .reduce((s1, s2) -> s1 + "#" + s2); reduced.ifPresent(System.out::println); // "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

병렬 스트림

위에서 언급한 것처럼 스트림은 순차적일 수도 있고 병렬적일 수도 있습니다. 순차적 스트림 작업은 직렬 스레드에서 수행되는 반면, 병렬 스트림 작업은 여러 병렬 스레드에서 수행됩니다. 다음 예에서는 병렬 스트림을 사용하여 성능을 쉽게 향상시키는 방법을 보여줍니다. 먼저 고유한 요소의 큰 목록을 만들어 보겠습니다. 이제 이 컬렉션의 스트림을 정렬하는 데 소요된 시간을 결정하겠습니다. int max = 1000000; List values = new ArrayList<>(max); for (int i = 0; i < max; i++) { UUID uuid = UUID.randomUUID(); values.add(uuid.toString()); }
직렬 스트림
long t0 = System.nanoTime(); long count = values.stream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("sequential sort took: %d ms", millis)); // sequential sort took: 899 ms
병렬 스트림
long t0 = System.nanoTime(); long count = values.parallelStream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("parallel sort took: %d ms", millis)); // parallel sort took: 472 ms 보시다시피 두 조각은 거의 동일하지만 병렬 정렬이 50% 더 빠릅니다. 필요한 것은 stream()을 parallelStream() 으로 변경하는 것뿐입니다 .

지도

이미 언급했듯이 맵은 스트림을 지원하지 않습니다. 대신, map은 일반적인 문제를 해결하기 위한 새롭고 유용한 방법을 지원하기 시작했습니다. 위의 코드는 직관적이어야 합니다. putIfAbsent는 추가 null 검사 작성에 대해 경고합니다. forEach는 각 맵 값에 대해 실행할 함수를 허용합니다. 이 예는 함수를 사용하여 맵 값에 대한 작업이 수행되는 방법을 보여줍니다. 다음으로 주어진 값에 매핑되는 경우에만 주어진 키에 대한 항목을 제거하는 방법을 배웁니다. 또 다른 좋은 방법: 맵 항목 병합 은 매우 쉽습니다. 주어진 키에 대한 항목이 없으면 키/값을 맵에 삽입하거나 병합 함수가 호출되어 기존 항목의 값을 변경합니다. Map map = new HashMap<>(); for (int i = 0; i < 10; i++) { map.putIfAbsent(i, "val" + i); } map.forEach((id, val) -> System.out.println(val)); map.computeIfPresent(3, (num, val) -> val + num); map.get(3); // val33 map.computeIfPresent(9, (num, val) -> null); map.containsKey(9); // false map.computeIfAbsent(23, num -> "val" + num); map.containsKey(23); // true map.computeIfAbsent(3, num -> "bam"); map.get(3); // val33 map.remove(3, "val3"); map.get(3); // val33 map.remove(3, "val33"); map.get(3); // null map.getOrDefault(42, "not found"); // not found map.merge(9, "val9", (value, newValue) -> value.concat(newValue)); map.get(9); // val9 map.merge(9, "concat", (value, newValue) -> value.concat(newValue)); map.get(9); // val9concat
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION