JavaRush /Blog Java /Random-VI /Các tính năng của Java 8 – Hướng dẫn cơ bản (Phần 2)
0xFF
Mức độ
Донецк

Các tính năng của Java 8 – Hướng dẫn cơ bản (Phần 2)

Xuất bản trong nhóm
Phần thứ hai trong bản dịch của bài viết Tính năng Java 8 – Hướng dẫn TUYỆT VỜI . Phần đầu tiên ở đây (liên kết có thể thay đổi). Các tính năng của Java 8 – Hướng dẫn cơ bản (Phần 2) - 1

5. Các tính năng mới trong thư viện Java 8

Java 8 đã thêm nhiều lớp mới và mở rộng các lớp hiện có để hỗ trợ tốt hơn cho hoạt động đồng thời hiện đại, lập trình chức năng, ngày/giờ, v.v.

5.1. Lớp tùy chọn

NullPointerException nổi tiếng cho đến nay là nguyên nhân phổ biến nhất gây ra lỗi ứng dụng Java. Cách đây rất lâu, dự án xuất sắc Guava của Google đã được trình bày Optionalnhư một giải pháp NullPointerException, nhờ đó ngăn chặn mã bị ô nhiễm do kiểm tra null và kết quả là khuyến khích việc viết mã sạch hơn. Lớp Guava lấy cảm hứng từ Google Optionalhiện là một phần của Java 8. OptionalNó chỉ là một vùng chứa: nó có thể chứa một giá trị hoặc một loại nào đó Тhoặc đơn giản là null. Nó cung cấp nhiều phương pháp hữu ích để việc kiểm tra null rõ ràng không còn hợp lý nữa. Tham khảo tài liệu chính thức để biết thêm thông tin chi tiết. Hãy xem xét hai ví dụ nhỏ về cách sử dụng Optional: có và không có null.
Optional<String> fullName = Optional.ofNullable( null );
System.out.println( "Full Name is set? " + fullName.isPresent() );
System.out.println( "Full Name: " + fullName.orElseGet( () -> "[none]" ) );
System.out.println( fullName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
Phương thức isPresent()trả về true nếu phiên bản Optionalchứa giá trị khác null và ngược lại là false . Phương thức này orElseGet()chứa cơ chế dự phòng cho kết quả nếu Optionalnó chứa null, chấp nhận các hàm để tạo giá trị mặc định. Phương thức map () biến đổi giá trị hiện tại Optionalvà trả về một thể hiện mới Optional. Phương thức này orElse()tương tự như orElseGet(), nhưng thay vì lấy một hàm, nó lấy một giá trị mặc định. Đây là đầu ra của chương trình này:
Full Name is set? false
Full Name: [none]
Hey Stranger!
Chúng ta hãy xem nhanh một ví dụ khác:
Optional<String> firstName = Optional.of( "Tom" );
System.out.println( "First Name is set? " + firstName.isPresent() );
System.out.println( "First Name: " + firstName.orElseGet( () -> "[none]" ) );
System.out.println( firstName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
System.out.println();
Kết quả sẽ như thế này:
First Name is set? true
First Name: Tom
Hey Tom!
Để biết thêm thông tin chi tiết, vui lòng tham khảo tài liệu chính thức .

5.2. Dòng

API Stream mới được thêm vào ( java.util.stream) giới thiệu cách lập trình kiểu chức năng thực sự trong Java. Cho đến nay, đây là sự bổ sung toàn diện nhất cho thư viện Java và cho phép các nhà phát triển Java hoạt động hiệu quả hơn đáng kể, đồng thời cũng cho phép họ tạo mã hiệu quả, rõ ràng và ngắn gọn. API Stream giúp việc xử lý các bộ sưu tập dễ dàng hơn nhiều (nhưng không giới hạn ở chúng, như chúng ta sẽ thấy sau). Hãy lấy một lớp đơn giản làm ví dụ Task.
public class Streams  {
    private enum Status {
        OPEN, CLOSED
    };

    private static final class Task {
        private final Status status;
        private final Integer points;

        Task( final Status status, final Integer points ) {
            this.status = status;
            this.points = points;
        }

        public Integer getPoints() {
            return points;
        }

        public Status getStatus() {
            return status;
        }

        @Override
        public String toString() {
            return String.format( "[%s, %d]", status, points );
        }
    }
}
Nhiệm vụ có một số ý nghĩa về điểm (hoặc độ khó giả) và có thể là OPEN hoặc CLOSE . Hãy giới thiệu một tập hợp nhỏ các vấn đề để giải quyết.
final Collection<Task> tasks = Arrays.asList(
    new Task( Status.OPEN, 5 ),
    new Task( Status.OPEN, 13 ),
    new Task( Status.CLOSED, 8 )
);
Câu hỏi đầu tiên mà chúng tôi dự định tìm hiểu là nhiệm vụ MỞ hiện có bao nhiêu điểm? Trước Java 8, giải pháp thông thường cho vấn đề này là sử dụng iterator foreach. Nhưng trong Java 8, câu trả lời là các luồng: một chuỗi các phần tử hỗ trợ các hoạt động tổng hợp tuần tự và song song.
// Подсчет общего количества очков всех активных задач с использованием sum()
final long totalPointsOfOpenTasks = tasks
    .stream()
    .filter( task -> task.getStatus() == Status.OPEN )
    .mapToInt( Task::getPoints )
    .sum();

System.out.println( "Total points: " + totalPointsOfOpenTasks );
Và đầu ra của giao diện điều khiển sẽ trông như sau:
Total points: 18
Chúng ta hãy nhìn vào những gì đang xảy ra ở đây. Đầu tiên, tập hợp tác vụ được chuyển đổi thành biểu diễn phát trực tuyến. Sau đó, thao tác filtersẽ lọc ra tất cả các tác vụ có trạng thái ĐÃ ĐÓNG . Trong bước tiếp theo, thao tác mapToIntchuyển đổi luồng Tasks thành luồng Integers bằng cách sử dụng một phương thức Task::getPointscho từng phiên bản Task. Cuối cùng, tất cả các điểm được tính tổng bằng phương pháp sum, phương pháp này cung cấp kết quả cuối cùng. Trước khi chuyển sang các ví dụ tiếp theo, có một số lưu ý về chủ đề cần ghi nhớ (chi tiết tại đây ). Các hoạt động streamđược chia thành các hoạt động trung giancuối cùng . Hoạt động trung gian trả về một luồng mới. Chúng luôn lười biếng; khi thực hiện các thao tác trung gian như filter, chúng không thực sự thực hiện lọc mà thay vào đó tạo một luồng mới, khi hoàn thành sẽ chứa các phần tử của luồng ban đầu khớp với vị từ đã cho. Các hoạt động hữu hạn , chẳng hạn như forEachsum, có thể được truyền qua một luồng để tạo ra kết quả hoặc tác dụng phụ. Sau khi hoàn thành thao tác cuối cùng, luồng được coi là đã sử dụng và không thể sử dụng lại. Trong hầu hết các trường hợp, các hoạt động cuối cùng có xu hướng hoàn thành quá trình truyền tải của chúng thông qua nguồn dữ liệu cơ bản. Một tính năng có giá trị khác của luồng là hỗ trợ các quy trình song song ngay lập tức. Hãy xem ví dụ này để tìm tổng điểm của tất cả các bài toán.
// Calculate total points of all tasks
final double totalPoints = tasks
   .stream()
   .parallel()
   .map( task -> task.getPoints() ) // or map( Task::getPoints )
   .reduce( 0, Integer::sum );

System.out.println( "Total points (all tasks): " + totalPoints );
Điều này rất giống với ví dụ đầu tiên, ngoại trừ việc chúng tôi đang cố gắng xử lý song song tất cả các tác vụ và tính toán kết quả cuối cùng bằng phương thức reduce. Đây là đầu ra của giao diện điều khiển:
Total points (all tasks): 26.0
Thường có nhu cầu nhóm các yếu tố theo một tiêu chí nhất định. Ví dụ này cho thấy các chủ đề có thể trợ giúp việc này như thế nào.
// Группировка задач по их статусу
final Map<Status, List<Task>> map = tasks
    .stream()
    .collect( Collectors.groupingBy( Task::getStatus ) );
System.out.println( map );
Đầu ra của bàn điều khiển sẽ như sau:
{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}
Để kết thúc với các ví dụ về bài toán, hãy tính tỷ lệ phần trăm (hoặc trọng số) tổng thể của từng bài toán trong tập hợp dựa trên tổng số điểm:
// Подсчет веса каждой задачи (How процент от общего количества очков)
final Collection<String> result = tasks
    .stream()                                        // Stream<String>
    .mapToInt( Task::getPoints )                     // IntStream
    .asLongStream()                                  // LongStream
    .mapToDouble( points -> points / totalPoints )   // DoubleStream
    .boxed()                                         // Stream<Double>
    .mapToLong( weigth -> ( long )( weigth * 100 ) ) // LongStream
    .mapToObj( percentage -> percentage + "%" )      // Stream<String>
    .collect( Collectors.toList() );                 // List<String>

System.out.println( result );
Đầu ra của bàn điều khiển sẽ như thế này:
[19%, 50%, 30%]
Cuối cùng, như chúng tôi đã lưu ý trước đó, API Stream không chỉ dành cho các bộ sưu tập Java. Một thao tác I/O điển hình, chẳng hạn như đọc từng dòng tệp văn bản, là một ứng cử viên rất phù hợp để sử dụng xử lý luồng. Đây là một ví dụ nhỏ để chứng minh điều này.
final Path path = new File( filename ).toPath();
try( Stream<String> lines = Files.lines( path, StandardCharsets.UTF_8 ) ) {
    lines.onClose( () -> System.out.println("Done!") ).forEach( System.out::println );
}
Phương thức onConsoleđược gọi trên một luồng sẽ trả về một luồng tương đương với một trình xử lý riêng bổ sung. Trình xử lý riêng được gọi khi một phương thức close()được gọi trên một luồng. API Stream cùng với lambdacác phương thức tham chiếu cùng với các phương thức mặc định và tĩnh trong Java 8 là câu trả lời cho các mô hình phát triển phần mềm hiện đại. Để biết thêm thông tin chi tiết, vui lòng tham khảo tài liệu chính thức .

5.3. API ngày/giờ (JSR 310)

Java 8 mang đến giao diện mới cho việc quản lý ngày và giờ bằng cách cung cấp API Ngày và Giờ mới (JSR 310) . Thao tác ngày và giờ là một trong những điểm khó khăn nhất đối với các nhà phát triển Java. Tiêu chuẩn java.util.Datesau đây java.util.Calendarnhìn chung không cải thiện được tình hình (thậm chí có thể khiến nó trở nên khó hiểu hơn). Đó là cách Joda-Time ra đời : một giải pháp thay thế API ngày/giờ tuyệt vời cho Java . API Ngày/Giờ mới trong Java 8 (JSR 310) chịu ảnh hưởng nặng nề bởi Joda-Time và tận dụng tối đa nó. Gói mới java.timechứa tất cả các lớp về ngày, giờ, ngày/giờ, múi giờ, khoảng thời gian và thao tác thời gian . Thiết kế API rất coi trọng tính bất biến: không được phép thay đổi (một bài học đắt giá rút ra từ java.util.Calendar). Nếu cần sửa đổi, một phiên bản mới của lớp tương ứng sẽ được trả về. Hãy xem xét các lớp chính và ví dụ về việc sử dụng chúng. Lớp đầu tiên Clock, cung cấp quyền truy cập vào thời điểm, ngày và giờ hiện tại bằng cách sử dụng múi giờ. Clockcó thể được sử dụng thay vì System.currentTimeMillis()TimeZone.getDefault().
// Получить системное время How смещение UTC
final Clock clock = Clock.systemUTC();
System.out.println( clock.instant() );
System.out.println( clock.millis() );
Đầu ra bảng điều khiển ví dụ:
2014-04-12T15:19:29.282Z
1397315969360
Các lớp mới khác mà chúng tôi sẽ xem xét là LocaleDateLocalTime. LocaleDatechỉ chứa phần ngày không có múi giờ trong hệ thống lịch ISO-8601. Theo đó, LocalTimenó chỉ chứa một phần timecode>.
// получить местную date и время время
final LocalDate date = LocalDate.now();
final LocalDate dateFromClock = LocalDate.now( clock );

System.out.println( date );
System.out.println( dateFromClock );

// получить местную date и время время
final LocalTime time = LocalTime.now();
final LocalTime timeFromClock = LocalTime.now( clock );

System.out.println( time );
System.out.println( timeFromClock );
Đầu ra bảng điều khiển ví dụ:
2014-04-12
2014-04-12
11:25:54.568
15:25:54.568
LocalDateTimenối LocaleDateLocalTimechứa ngày và giờ, nhưng không có múi giờ, trong hệ thống lịch ISO-8601. Một ví dụ đơn giản được đưa ra dưới đây.
// Get the local date/time
final LocalDateTime datetime = LocalDateTime.now();
final LocalDateTime datetimeFromClock = LocalDateTime.now( clock );

System.out.println( datetime );
System.out.println( datetimeFromClock );
Đầu ra bảng điều khiển ví dụ:
2014-04-12T11:37:52.309
2014-04-12T15:37:52.309
Trong trường hợp bạn cần ngày/giờ cho một múi giờ cụ thể, ZonedDateTime. Nó chứa ngày và giờ trong hệ thống lịch ISO-8601. Dưới đây là một số ví dụ cho các múi giờ khác nhau.
// Получение даты/времени для временной зоны
final ZonedDateTime zonedDatetime = ZonedDateTime.now();
final ZonedDateTime zonedDatetimeFromClock = ZonedDateTime.now( clock );
final ZonedDateTime zonedDatetimeFromZone = ZonedDateTime.now( ZoneId.of( "America/Los_Angeles" ) );

System.out.println( zonedDatetime );
System.out.println( zonedDatetimeFromClock );
System.out.println( zonedDatetimeFromZone );
Đầu ra bảng điều khiển ví dụ:
2014-04-12T11:47:01.017-04:00[America/New_York]
2014-04-12T15:47:01.017Z
2014-04-12T08:47:01.017-07:00[America/Los_Angeles]
Và cuối cùng, chúng ta hãy xem lớp Duration: khoảng thời gian tính bằng giây và nano giây. Điều này làm cho việc tính toán giữa hai ngày rất đơn giản. Hãy xem cách thực hiện việc này:
// Получаем разницу между двумя датами
final LocalDateTime from = LocalDateTime.of( 2014, Month.APRIL, 16, 0, 0, 0 );
final LocalDateTime to = LocalDateTime.of( 2015, Month.APRIL, 16, 23, 59, 59 );

final Duration duration = Duration.between( from, to );
System.out.println( "Duration in days: " + duration.toDays() );
System.out.println( "Duration in hours: " + duration.toHours() );
Ví dụ trên tính toán khoảng thời gian (theo ngày và giờ) giữa hai ngày, 16 tháng 4 năm 201416 tháng 4 năm 2015 . Đây là một ví dụ về đầu ra của bàn điều khiển:
Duration in days: 365
Duration in hours: 8783
Ấn tượng chung về ngày/giờ mới trong Java 8 là rất, rất tích cực. Một phần vì những thay đổi dựa trên nền tảng đã được thử nghiệm trong trận chiến (Joda-Time), một phần vì lần này vấn đề đã được xem xét lại một cách nghiêm túc và tiếng nói của các nhà phát triển đã được lắng nghe. Để biết chi tiết, vui lòng tham khảo tài liệu chính thức .

5.4. Công cụ JavaScript Nashorn

Java 8 đi kèm với công cụ JavaScript Nashorn mới , cho phép bạn phát triển và chạy một số loại ứng dụng JavaScript nhất định trên JVM. Công cụ JavaScript Nashorn chỉ đơn giản là một triển khai khác của javax.script.ScriptEngine tuân theo cùng một bộ quy tắc để cho phép Java và JavaScript tương tác. Đây là một ví dụ nhỏ.
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName( "JavaScript" );

System.out.println( engine.getClass().getName() );
System.out.println( "Result:" + engine.eval( "function f() { return 1; }; f() + 1;" ) );
Đầu ra bảng điều khiển ví dụ:
jdk.nashorn.api.scripting.NashornScriptEngine
Result: 2

5.5. cơ sở64

Cuối cùng, hỗ trợ mã hóa Base64 đã được đưa vào thư viện chuẩn Java với việc phát hành Java 8. Nó rất dễ sử dụng, ví dụ minh họa điều này.
package com.javacodegeeks.java8.base64;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class Base64s {
    public static void main(String[] args) {
        final String text = "Base64 finally in Java 8!";

        final String encoded = Base64
            .getEncoder()
            .encodeToString( text.getBytes( StandardCharsets.UTF_8 ) );
        System.out.println( encoded );

        final String decoded = new String(
            Base64.getDecoder().decode( encoded ),
            StandardCharsets.UTF_8 );
        System.out.println( decoded );
    }
}
Đầu ra bảng điều khiển của chương trình hiển thị cả văn bản được mã hóa và giải mã:
QmFzZTY0IGZpbmFsbHkgaW4gSmF2YSA4IQ==
Base64 finally in Java 8!
Ngoài ra còn có các lớp dành cho bộ mã hóa/giải mã thân thiện với URL cũng như bộ mã hóa/giải mã thân thiện với MIME ( Base64.getUrlEncoder()/ Base64.getUrlDecoder(), Base64.getMimeEncoder()/ Base64.getMimeDecoder()).

5.6. Mảng song song

Bản phát hành Java 8 bổ sung thêm nhiều phương thức mới để xử lý mảng song song. Có lẽ điều quan trọng nhất trong số này là parallelSort(), có thể tăng tốc đáng kể việc sắp xếp trên các máy đa lõi. Ví dụ nhỏ dưới đây minh họa nhóm phương thức mới ( parallelXxx) đang hoạt động.
package com.javacodegeeks.java8.parallel.arrays;

import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;

public class ParallelArrays {
    public static void main( String[] args ) {
        long[] arrayOfLong = new long [ 20000 ];

        Arrays.parallelSetAll( arrayOfLong,
            index -> ThreadLocalRandom.current().nextInt( 1000000 ) );
        Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
            i -> System.out.print( i + " " ) );
        System.out.println();

        Arrays.parallelSort( arrayOfLong );
        Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
            i -> System.out.print( i + " " ) );
        System.out.println();
    }
}
Đoạn mã nhỏ này sử dụng một phương thức parallelSetAll()để điền vào một mảng với 20.000 giá trị ngẫu nhiên. Sau này nó được áp dụng parallelSort(). Chương trình in 10 phần tử đầu tiên trước và sau khi sắp xếp để cho thấy mảng thực sự đã được sắp xếp. Đầu ra của chương trình ví dụ có thể trông như thế này (Lưu ý rằng các phần tử của mảng là ngẫu nhiên).
Unsorted: 591217 891976 443951 424479 766825 351964 242997 642839 119108 552378
Sorted: 39 220 263 268 325 607 655 678 723 793

5.7. Sự song song

Các phương thức mới đã được thêm vào lớp java.util.concurrent.ConcurrentHashMapđể hỗ trợ các hoạt động tổng hợp dựa trên các đối tượng luồng và biểu thức lambda mới được thêm vào. Ngoài ra, các phương thức mới đã được thêm vào lớp java.util.concurrent.ForkJoinPoolđể hỗ trợ tính năng gộp chung (xem thêm khóa học miễn phí của chúng tôi về tính tương tranh Java ). Một lớp mới java.util.concurrent.locks.StampedLockđã được thêm vào để cung cấp khóa dựa trên khả năng với ba chế độ truy cập để kiểm soát đọc/ghi (nó có thể được coi là một giải pháp thay thế tốt hơn cho chế độ không tốt lắm java.util.concurrent.locks.ReadWriteLock). Các lớp mới đã được thêm vào gói java.util.concurrent.atomic:
  • Tích lũy kép
  • Bộ cộng đôi
  • Ắc quy dài
  • LongAdder

6. Các tính năng mới trong môi trường thời gian chạy Java (JVM)

Khu vực này PermGenđã ngừng hoạt động và được thay thế bởi Metaspace (JEP 122). Các tùy chọn JVM -XX:PermSize-XX:MaxPermSizeđã được thay thế bằng -XX:MetaSpaceSize-XX:MaxMetaspaceSizetương ứng.

7. Kết luận

Tương lai là đây: Java 8 đã đưa nền tảng của mình tiến lên phía trước bằng cách cung cấp các tính năng cho phép các nhà phát triển làm việc hiệu quả hơn. Vẫn còn quá sớm để chuyển các hệ thống sản xuất sang Java 8, nhưng việc áp dụng sẽ bắt đầu tăng dần trong vài tháng tới. Tuy nhiên, bây giờ là lúc bắt đầu chuẩn bị cơ sở mã của bạn để tương thích với Java 8 và sẵn sàng kết hợp các thay đổi của Java 8 khi nó đủ an toàn và ổn định. Như một minh chứng cho sự chấp nhận của cộng đồng đối với Java 8, Pivotal gần đây đã phát hành Spring Framework với sự hỗ trợ sản xuất cho Java 8 . Bạn có thể cung cấp ý kiến ​​của mình về các tính năng mới thú vị trong Java 8 trong phần nhận xét.

8. Nguồn

Một số tài nguyên bổ sung thảo luận sâu về các khía cạnh khác nhau của các tính năng của Java 8:
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION