每种编程语言都提供了表达我们的想法并将其转化为现实的方法。
有些是该特定语言所独有的,有些是许多其他编程语言所共有的。
在本文中,我将探讨开发人员在日常编程工作中经常使用的十个 Java 编程特性。
集合的工厂方法
集合是我们日常编码中最常用的功能。它们被用作我们存储对象并传递它们的容器。
集合还用于对对象进行排序、搜索和迭代,使程序员的生活更轻松。它提供了一些基本的接口,如List、Set、Map等,以及多种实现。
传统的创建方式对许多开发人员Collections来说Maps可能看起来很冗长。
这就是为什么 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");
尽管这似乎是一个方便的功能,但也有一些批评。一些开发人员会争辩说,这可能会降低可读性,这比这个小便利更重要。
要了解更多信息,请访问:
打开 JDK Lvti-Faq
打开 JDK Lvti-style-guide
增强的开关表达式
传统的 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;};
阅读有关它的更多信息:使用 Switch 表达式进行分支
记录
尽管记录是 Java 中相对较新的功能,在 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) {}
可选的
方法是一种契约:我们在定义它时会考虑到它。我们指定参数及其类型以及返回类型。当我们调用一个方法时,我们期望它按照约定行事。如果没有,则违反合同。
但是,我们经常从方法中获取 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(); }}
现在我们findName()用 Optional 重写了方法,它指定了不返回任何值的可能性,我们可以处理它。这会向程序员发出预先警告并修复违规行为。
Java 日期时间 API
每个开发人员都在某种程度上对日期时间计算感到困惑。这不是夸大其词。这主要是由于长期以来没有一个好的 Java API 来处理 Java 中的日期和时间。
然而,这个问题不再存在,因为 Java 8 在java.time包中带来了一套出色的 API,可以解决所有与日期和时间相关的问题。
java.time包提供了许多接口和类来解决处理日期和时间的大多数问题,包括时区(有时会非常复杂)。但是,我们主要使用以下类 -
- 本地日期
- 当地时间
- 本地日期时间
- 期间
- 时期
- ZonedDateTime 等
这些类旨在具有通常需要的所有方法。例如
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}
看上面代码的main方法。我们可以看到我们会得到一个空指针异常。如果我们使用 pre-Java 14 运行和编译代码,我们将获得以下 StackTrace:
Exception in thread "main" java.lang.NullPointerExceptionat 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 nullat ca.bazlur.playground.Main.getLengthOfUsersName(Main.java:12)at ca.bazlur.playground.Main.main(Main.java:8)
可完成的未来
我们逐行编写程序,通常它们会逐行执行。但是,有时我们希望相对并行执行以使程序更快。为此,我们通常参考 Java 线程。
好吧,Java 线程编程并不总是与并行编程有关。相反,它为我们提供了一种组合程序的多个独立单元以独立执行以与其他单元一起取得进展的方法,并且它们通常是异步运行的。
然而,线程编程及其复杂性似乎很可怕。大多数初级和中级开发人员都在为此苦苦挣扎。这就是为什么 Java 8 带来了一个更直接的 API,让我们可以完成程序的一部分异步运行。让我们看一个例子:
假设我们必须调用三个 REST API,然后组合结果。我们可以一一称呼。如果它们每个都需要大约 200 毫秒,那么获取所有它们的总时间将需要 600 毫秒。
如果我们可以并行运行它们会怎样?由于现代 CPU 中包含多核,因此它们可以轻松处理三个不同 CPU 上的三个休息调用。使用 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 表达式可能是 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 表达式还有很多其他好处。
流 API
“Lambda 表达式是通向 Java 8 的入门药物,但 Streams 才是真正的瘾。”- 文卡特·苏布拉马尼亚姆。
在我们的日常编程工作中,我们经常做的一项常见任务是处理一组数据。有一些常见的操作,例如过滤、转换和收集结果。
在 Java 8 之前,这类操作本质上是必不可少的。我们必须为我们的意图(也就是我们想要实现的目标)以及我们想要的方式编写代码。
随着 Lambda 表达式和流 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();}
虽然这两种方法返回相同的值,但我们清楚地看到了区别。