10 大 Java 语言特性

十大 Java 语言特性

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

其中一些特性是某些语言独有的,而另一些特性则是大多数语言相通的。

在本文中,我们将探讨开发人员在日常编程工作中经常使用的十个 Java 语言特性。

Collection 的工厂方法

Collection 是我们每天写代码中最常用到的特性。它用于作为一种储存和传递多个对象的容器。

Collection 也能用于排序、搜索和遍历对象,让我们的工作更轻松些。它提供了几个基本的接口,如 List 、 Set 、 Map 等。

对于很多开发者来说,传统的创建 Map 的方法可能看起来很冗长。

正因如此,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 引入了局部变量类型推断(LVTI)。这对开发者来说真的非常方便!

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

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

在上方代码的中,我们在语句的左右两边都指明了变量类型。如果我们在一个地方定义它,我们很容易理解这必须是一个 Map 类型。Java 语言已经很成熟了,编译器应该足够智能地去识别这一点。LVTI 特性做的正是这一点。上方的代码可以这样写:

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

现在我们只需要写一次类型了。这似乎也没好太多。但是,当我们调用方法并将结果存储在变量中时,它会缩短很多。例如:

var properties = getProperties();

类似地,

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

虽然这看起来是个方便的特性,但它也备受诟病。一些开发者认为:LVTI 可能会降低可读性,这可比那一点点的便利重要得多。

增强的 switch 语句

传统的 switch 语句从一开始就存在了,类似于 C 和 C++。过去,它没啥问题,但随着语言的发展,在 Java 14 发布之前,它一直都没有什么改进。 switch 语句也确实一直存在一些局限性,其中最臭名昭著的就属穿透问题

为了解决这个问题,我们需要使用许多的 break 语句,它们几乎成为了模板代码。然而,Java 14 引入了一个看待 switch 语句的方式,并提供了更丰富的功能。

我们不再需要添加 break 语句,新特性解决了穿透问题。最重要的是, switch 语句可以返回值了。这意味着我们可以将 switch 语句作为一个表达式并赋值给变量。

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

Record 类

尽管 Record 类是 Java 中相对较新的功能(在 Java 16 中发布),但许多开发人员发现在创建不可变的 Record 对象非常有用。

通常,我们需要在程序中使用数据载体对象来保存或将值从一种方法传递到另一种方法。举例来说,一个带有 x、y、z 轴数据的类可以这么写:

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 + ']';
    }
}

 

整个类看起来超级冗长且和我们的想实现的东西关系不大。可以将上面的代码改成下面的写法:

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

Optional 类

方法是一种约定:在定义方法时我们需要考虑到这一点。我们指定了一个方法的参数以及返回类型。当我们调用它时,我们期望它按照约定行事。如果没有,它则违反了约定。

然而,我们经常从一个方法中得到返回值 null ,而不是先前所指定的类型。这是一种打破规矩的行为。调用者不能预先知道,除非它调用了该方法。为了解决这个问题,调用者通常用一个 if 条件来测试返回值是否为 null 。例子:

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() 方法,说明了方法有不返回任何值的可能性。这给了程序员一个预先的警告,并解决了违反约定的问题。

日期和时间的 API

每个开发人员都在某种程度上对日期和时间计算感到困惑。我所说的并不夸张。这主要是由于长期以来没有一个好的 Java API 来处理日期和时间。

然而,这个问题已经不复存在,因为 Java 8 在 java.time 包中带来了一套优秀的 API,解决了所有与日期和时间有关的问题。

java.time包提供了许多的接口和类,解决了大多数处理日期和时间的问题,包括时区(在某些时候,这东西是令人抓狂的复杂)。其中常用的类有:

LocalDate
LocalTime
LocalDateTime
Duration
Period
ZonedDateTime 

这些类囊括了所有常用的方法,例如:

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

public class Playground {
    
    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 的错误信息

每个开发者都痛恨空指针异常。当栈追踪没有提供任何有用的信息时,事情就变得更有挑战性了。为了演示这个问题,让我们来看一段代码:

package com.bazlur;

public class Playground {

    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
}

看看上方代码中的 main() 方法。我们可以预测到我们会得到一个空指针异常。如果我们在 Java 14 之前的环境中编译及运行这段代码,我们会得到下面这段栈追踪:

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 及更高的版本中,我们会在栈追踪中得到更多的信息,非常方便。

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 线程。

Java 线程编程并不总是与并行编程有关。相反,它提供了一种方法,使程序的多个单元能独立执行,与其他单元同时进展。不仅如此,它们通常是异步运行的。

可是,线程编程及其错综复杂的问题似乎很可怕。大多数开发人员都为此而挣扎。这就是为什么 Java 8 带来了一个更直接的 API,让我们完成部分程序的异步运行。让我们看一个例子。

假设我们要调用三个 REST API,然后把结果组合起来。我们可以逐个调用它们。如果它们每个都需要 200 毫秒左右,那么获取所有结果的总时间就是 600 毫秒。

如果我们并行地运行它们呢?由于现代的 CPU 有多个核,它可以很容易地在不同的核上处理三个 REST 调用。使用 CompletableFuture ,我们可以很轻易地完成这个任务。

 

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 表达式或许是 Java 语言中最强大的特性。它重塑了我们编写代码的方式。一个 lambda 表达式是一个接受参数并返回值的匿名函数。

我们可以将函数赋值给一个变量,也可以将其作为参数传递给方法。lambda 表达式有函数体,和方法唯一的差别是它没有名字。

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 才是真正的处方。” —— Venkat Subramaniam

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

在 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();
}

虽然两个方法都返回了相同的值,但两者之间的差别是显而易见的。

这就是今天的全部内容。拜拜! 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值