十大Java语言特性

129 阅读10分钟

让我们探索开发人员在日常编程工作中经常使用的十个Java编程功能。

每一种编程语言都提供了表达我们想法的方法,然后将其转化为现实。

有些是该特定语言所独有的,有些是许多其他编程语言所共有的。

在本文中,我将探索开发人员在日常编程工作中经常使用的十个Java编程功能。

收集的工厂方法

集合是我们日常编码中最常用的特性。它们被用作存储和传递对象的容器。

集合还用于对对象进行排序、搜索和迭代,使程序员的工作更轻松。它提供了一些基本接口,如List、Set、Map等,以及多种实现。

对于许多开发人员来说,创建CollectionsMaps的传统方法可能看起来冗长。

这就是为什么Java 9引入了一些非常简洁的工厂方法。

List:

List countries = List.of("Bangladesh", "Canada", "United States", "Tuvalu"); 

Set:

Set countries = Set.of("Bangladesh", "Canada", "United States", "Tuvalu"); 

Map:

Map<String, Integer> countriesByPopulation = Map.of("Bangladesh", 164_689_383,
                                                            "Canada", 37_742_154,
                                                            "United States", 331_002_651,
                                                            "Tuvalu", 11_792);

当我们想要创建不可变的容器时,这些非常方便。然而,如果我们要创建可变集合,建议使用传统方法。

如果您想了解有关集合框架的更多信息,请访问此处:集合框架

本地类型推断

Java 10引入了局部变量的类型推断,这对开发人员来说非常方便。

传统上,Java是一种强类型语言,开发人员在声明和初始化对象时必须指定两次类型。这看起来很乏味。看看以下示例:

Map<String, Map<String, Integer>> properties = new HashMap<>();

我们在上述声明中指定了双方的信息类型。如果我们在一个地方定义它,我们的眼睛可以很容易地解释这必须是一个Map类型。Java语言已经足够成熟了,Java编译器应该足够聪明,能够理解这一点。局部类型推断正是这样做的。

上面的代码现在可以这样写:

var properties = new HashMap<String, Map<String, Integer>>();

现在我们必须写并输入一次。上面的代码看起来可能也很糟糕。然而,当我们调用一个方法并将结果存储在一个变量中时,它会使它短得多。例子:

var properties = getProperties();

同样

var countries = Set.of("Bangladesh", "Canada", "United States", "Tuvalu");

虽然这看起来是一个方便的功能,但也有一些批评。一些开发人员会争辩说,这可能会降低可读性,而可读性比这种小小的便利更重要。

如欲了解更多详情,请浏览:

增强的开关表达式

传统的switch语句从一开始就存在于Java中,类似于C和c++。这是可以的,但是随着语言的发展,它直到Java 14才为我们提供任何改进。当然,它也有一些局限性。最臭名昭著的是失败:

为了解决这个问题,我们使用break语句,这几乎是样板代码。但是,Java 14引入了一种查看switch语句的新方法,它提供了许多更丰富的特性。

我们不再需要添加break语句;它解决了失败的问题。最重要的是,switch语句可以返回一个值,这意味着我们可以将它用作表达式并将其赋值给一个变量。

int day = 5;
String result = switch (day) {
    case 1, 2, 3, 4, 5 -> "Weekday";
    case 6, 7 -> "Weekend";
    default -> "Unexpected value: " + day;
};

Records

虽然 records 是Java 16中发布的相对较新的功能,但许多开发人员发现创建不可变对象非常有帮助。

通常,我们需要程序中的数据职业对象来保存或传递从一种方法到另一种方法的值。例如,一个携带x、y和z坐标的类,我们会写如下。

package ca.bazlur.playground;

import java.util.Objects;

public final class Point {
    private final int x;
    private final int y;
    private final int z;

    public Point(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public int x() {
        return x;
    }

    public int y() {
        return y;
    }

    public int z() {
        return z;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) return true;
        if (obj == null || obj.getClass() != this.getClass()) return false;
        var that = (Point) obj;
        return this.x == that.x &&
                this.y == that.y &&
                this.z == that.z;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y, z);
    }

    @Override
    public String toString() {
        return "Point[" +
                "x=" + x + ", " +
                "y=" + y + ", " +
                "z=" + z + ']';
    }

}

这个类似乎非常冗长,而且与我们的整个意图没有什么关系。这整个代码可以用以下代码替换

package ca.bazlur.playground;

public record Point(int x, int y, int z) {
}

Optional

方法是一个契约:我们在定义一个契约时投入了思想。我们用参数的类型和返回类型指定参数。当我们调用一个方法时,我们希望它的行为符合契约。如果没有,那就违反了合同。

但是,我们经常从方法中获得null,而不是指定类型的值。这是违规行为。除非调用它,否则调用者无法预先知道。要处理此违规,调用方通常使用if条件测试该值,以确定该值是否为空。例子:

public class Playground {

    public static void main(String[] args) {
        String name = findName();
        if (name != null) {
            System.out.println("Length of the name : " + name.length());
        }
    }

    public static String findName() {
        return null;
    }
}

看看上面的代码。findName()方法应该返回一个String值,但它返回null。调用者现在必须先检查空值才能处理它。如果调用忘记这样做,他们最终将获得NullPointerException,这不是预期的行为。

另一方面,如果方法签名将指定无法返回值的可能性,它将解决所有混淆。这就是Optional发挥作用的地方。

import java.util.Optional;

public class Playground {

    public static void main(String[] args) {
        Optional<String> optionalName = findName();
        optionalName.ifPresent(name -> {
            System.out.println("Length of the name : " + name.length());
        });
    }

    public static Optional<String> findName() {
        return Optional.empty();
    }
}

现在我们已经用Optional重写了findName()方法,它指定了不返回任何值的可能性,我们可以处理它。这为程序员提供了一个预先警告,并修复了违规。

Java日期时间API

每个开发人员都会在某种程度上混淆日期-时间计算。这并不是夸大其词。这主要是因为很长一段时间以来没有一个很好的Java API来处理Java中的日期和时间。

但是,这个问题已经不存在了,因为Java 8在Java中带来了一个优秀的API集。java.time,解决所有日期和时间相关的问题。

java.time软件包提供了许多接口和类,解决了处理日期和时间的大多数问题,包括时区(这在某个时候非常复杂)。然而,我们主要使用以下课程-

  • LocalDate
  • LocalTime
  • LocalDateTime
  • Duration
  • Period
  • ZonedDateTime etc.

这些类旨在具有所有通常需要的方法,例如

import java.time.LocalDate;
import java.time.Month;

public class Playground3 {
    public static void main(String[] args) {
        LocalDate date = LocalDate.of(2022, Month.APRIL, 4);
        System.out.println("year = " + date.getYear());
        System.out.println("month = " + date.getMonth());
        System.out.println("DayOfMonth = " + date.getDayOfMonth());
        System.out.println("DayOfWeek = " + date.getDayOfWeek());
        System.out.println("isLeapYear = " + date.isLeapYear());
    }
}

同样,LocalTime拥有计算时间所需的所有方法。

LocalTime time = LocalTime.of(20, 30);
int hour = time.getHour(); 
int minute = time.getMinute(); 
time = time.withSecond(6); 
time = time.plusMinutes(3);

我们可以将两者结合起来:

LocalDateTime dateTime1 = LocalDateTime.of(2022, Month.APRIL, 4, 20, 30);
LocalDateTime dateTime2 = LocalDateTime.of(date, time);

我们如何包含时区:

ZoneId zone = ZoneId.of("Canada/Eastern");
LocalDate localDate = LocalDate.of(2022, Month.APRIL, 4);
ZonedDateTime zonedDateTime = date.atStartOfDay(zone);

有用的NullPointerException

每个开发人员都讨厌空指针异常。当StackTrace不提供有用的信息时,它就变得具有挑战性。为了演示问题,让我们看一个例子:

package com.bazlur;

public class Main {

    public static void main(String[] args) {
        User user = null;
        getLengthOfUsersName(user);
    }

    public static void getLengthOfUsersName(User user) {
        System.out.println("Length of first name: " + user.getName().getFirstName());
    }
}

class User {
    private Name name;
    private String email;

    public User(Name name, String email) {
        this.name = name;
        this.email = email;
    }

   //getter
   //setter
}

class Name {
    private String firstName;
    private String lastName;

    public Name(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

   //getter
   //setter
}

看看上面代码的主要方法。我们可以看到,我们将获得一个空指针异常。如果我们使用Java 14前运行和编译代码,我们将获得以下StackTrace:

Exception in thread "main" java.lang.NullPointerException
at com.bazlur.Main.getLengthOfUsersName(Main.java:11)
at com.bazlur.Main.main(Main.java:7)

此堆栈跟踪还可以,但它没有太多关于此NullPointerException发生地点和原因的信息。

然而,在Java 14及以后,我们在堆栈跟踪中获取更多信息,这非常方便。在Java 14中,我们将获得:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "ca.bazlur.playground.User.getName()" because "user" is null
at ca.bazlur.playground.Main.getLengthOfUsersName(Main.java:12)
at ca.bazlur.playground.Main.main(Main.java:8)

CompletableFuture

我们一行一行地编写程序,通常它们会一行一行地执行。然而,有时我们需要相对并行的执行来让程序更快。为了实现这一点,我们通常会使用Java Thread。

Java线程编程并不总是关于并行编程。相反,它为我们提供了一种方法,将程序的多个独立单元组合在一起,让它们独立执行,以便与其他单元一起前进,而且它们通常是异步运行的。

然而,线程编程及其复杂性似乎很可怕。大多数初级和中级开发人员都在与之斗争。这就是为什么Java 8带来了一个更直接的API,让我们能够异步运行程序的一部分。让我们来看一个例子:

让我们假设我们必须调用三个REST api,然后结合结果。我们可以逐个调用它们。如果每个请求都需要200毫秒,那么获取所有请求的总时间将需要600毫秒。

如果我们能让它们并行运行呢?由于现代cpu中有多核,它们可以轻松地在三个不同的cpu上处理三个rest调用。使用CompletableFuture,我们可以很容易地做到这一点。

package ca.bazlur.playground;

import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

public class SocialMediaService {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        var service = new SocialMediaService();

        var start = Instant.now();
        var posts = service.fetchAllPost().get();
        var duration = Duration.between(start, Instant.now());

        System.out.println("Total time taken: " + duration.toMillis());
    }

    public CompletableFuture<List<String>> fetchAllPost() {
        var facebook = CompletableFuture.supplyAsync(this::fetchPostFromFacebook);
        var linkedIn = CompletableFuture.supplyAsync(this::fetchPostFromLinkedIn);
        var twitter = CompletableFuture.supplyAsync(this::fetchPostFromTwitter);

        var futures = List.of(facebook, linkedIn, twitter);

        return CompletableFuture.allOf(futures.toArray(futures.toArray(new CompletableFuture[0])))
                .thenApply(future -> futures.stream()
                        .map(CompletableFuture::join)
                        .toList());
    }
    private String fetchPostFromTwitter() {
        sleep(200);
        return "Twitter";
    }

    private String fetchPostFromLinkedIn() {
        sleep(200);
        return "LinkedIn";
    }

    private String fetchPostFromFacebook() {
        sleep(200);
        return "Facebook";
    }

    private void sleep(int millis) {
        try {
            TimeUnit.MILLISECONDS.sleep(millis);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

Lambda表达式

Lambda Expression可能是Java语言中最强大的功能。它重塑了我们编写代码的方式。Lambda表达式就像一个匿名函数,可以接受参数并返回值。

我们可以将函数赋值给一个变量,并将它作为参数传递给一个方法,方法可以返回它。它有一个主体。与方法的唯一区别是它没有名称。

这些表达短小精悍。它通常不包含太多的样板代码。让我们来看一个例子:

我们想要列出一个目录中扩展名为.java的所有文件。

var directory = new File("./src/main/java/ca/bazlur/playground");
String[] list = directory.list(new FilenameFilter() {
    @Override
    public boolean accept(File dir, String name) {
        return name.endsWith(".java");
    }
});

如果仔细查看这段代码,就会发现我们向方法list()传递了一个匿名内部类。在内部类中,我们放入了过滤文件的逻辑。

本质上,我们感兴趣的是这段逻辑,而不是围绕逻辑的样板文件。

实际上,lambda表达式允许我们删除所有的样板文件,我们可以编写我们关心的代码。例子:

var directory = new File("./src/main/java/ca/bazlur/playground");
String[] list = directory.list((dir, name) -> name.endsWith(“.java"));

好吧,我刚刚在这里向您展示了一个例子,但lambda表达还有很多其他好处。

Stream API

在我们的日常编程工作中,我们经常做的一项常见任务是处理数据收集。有一些常见的操作,例如过滤、转换和收集结果。

在Java 8之前,这些操作本质上是必不可少的。我们必须为我们的意图(又名我们希望实现的目标)以及我们想要的方式编写代码。

随着Lambda表达式和Stream API的发明,我们现在可以相当声明地编写数据处理功能。我们只指定我们的意图,但我们不必写下我们如何获得结果。让我们看一个例子:

我们有一本书的列表,我们希望找到所有Java书籍的名称以逗号分隔和排序。

public static String getJavaBooks(List<Book> books) {
    return books.stream()
            .filter(book -> Objects.equals(book.language(), "Java"))
            .sorted(Comparator.comparing(Book::price))
            .map(Book::name)
            .collect(Collectors.joining(", "));
}

上述代码简单、可读且简洁。另一种命令式代码将是-

public static String getJavaBooksImperatively(List<Book> books) {
    var filteredBook = new ArrayList<Book>();
    for (Book book : books) {
        if (Objects.equals(book.language(), "Java")){
            filteredBook.add(book);
        }
    }
    filteredBook.sort(new Comparator<Book>() {
        @Override
        public int compare(Book o1, Book o2) {
            return Integer.compare(o1.price(), o2.price());
        }
    });

    var joiner = new StringJoiner(",");
    for (Book book : filteredBook) {
        joiner.add(book.name());
    }
    
    return joiner.toString();
}

虽然这两种方法都返回相同的值,但我们清楚地看到了差异。了解有关流API的更多信息。今天就到这里。