【Java】十个需要知道的特性
本文,将会介绍Java中常用的十种编程的特性。
集合工厂方法
集合是开发中常用的功能。它们像是一个容器,我们将数据存储在这个容器中。
集合还可以用于排序,搜索以及迭代循环,这种特性让开发人员工作更轻松。Java中,提供了集中基本的接口,比如:List
,Set
,Map
等等。
对于许多人来说,创建 list
或者 Maps
看起来会有点繁琐。
Java 9为我们引入了几个简洁的集合工厂方法,
List:
List listTest = List.of("a", "b", "c", "d");
Set:
Set setTest = Set.of("a", "b", "c", "d");
Map:
Map<String, Integer> mapTest = Map.of("a", 164_689_383,
"b", 37_742_154,
"c", 331_002_651,
"d", 11_792);
当我们想要创建不变的容器的时候,这些方法是很方便的。但是,如果我们要创建可变的集合,最好还是用传统方法。
如果想要了解更多的集合框架,可以访问这里: The collection framework.
本地类型判断
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表达式
Switch
语句是Java最基础的语句,功能跟C或者C++的类似,这当然没啥问题,但是随着语言的发展,问题还是暴露出来了,最可能出现的就是fall-through:
为了解决这个问题,我们需要在Case
中增加break
语句,这太麻烦了!太浪费时间,而且有可能会忘记增加。Java 14 引入了一种新方法来解决这个问题,并且提供了更加丰富的特性。
我们不在需要添加break
语句,他解决了fall-through
的问题。最重要的是,Switch
语句可以返回值,这意味着我们现在可以将其用作表达式,并且赋值给某些变量了。
int day = 5;
String result = switch (day) {
case 1, 2, 3, 4, 5 -> "Weekday";
case 6, 7 -> "Weekend";
default -> "Unexpected value: " + day;
};
- 如果要了解更多特性,可以看这里哦:Branching with Switch Expressions
Records
尽管Records
是Java 16发布的新特性,但许多的开发人员发现创建不可变对象的时候,它非常的有帮助。
通常,方法与方法之间我们需要数据载体对象来保持或者传递值,例如,一个类,有x,y,z
坐标的参数,我们这样写:
package cn.djc8.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 cn.djc8.playground;
public record Point(int x, int y, int z) {
}
- 查看更多内容,点击这里:Java record semantics.
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
。调用者必须先判断Null
才能往下走。如果忘记这么做,最终将会收获一个 NullPointerException
这不在程序设计的预期内。
另一方面,如果方法签名不返回签名指定的返回值,代码将会很混乱,看下面的代码,Optional
Show Time:
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()
方法,它指定了这个方法可能会返回一个不受控制的空值,我们可以去处理这种情况。这回提前向开发人员告知,开发人员将会针对这种情况做修复。
Java 日期时间 API
每个开发者都在某种程度上分不清日期跟时间计算的区别,我没有夸大其词,这主要是因为长期以来都没有个统一的Apil来集中处理Java中的时间跟日期。
但是,这个问题将不会再存在,相信现在很多人都已经基于Java 8或者11在做开发了,Java 8 引入了一个新的Apijava.time
可以解决所有与日期时间相关的问题。
java.time
包提供了许多接口跟类,这些接口跟类解决大多数处理日期跟时间的问题,包括时区(有时非常复杂)。但是,我们主要使用了以下的类:
- LocalDate
- LocalTime
- LocalDateTime
- Duration
- Period
- 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空指针异常
每个码农都讨厌空指针异常,当堆栈中不提供有帮助的信息的时候,它变得非常的难以理解。为了演示该问题,我们来看一个例子:
package cn.djc8;
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
方法,我们将会获得一个空指针异常的错误,如果我们在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)
这个堆栈看起来还可以,但是它不包括关于这个空指针异常发生的位置跟原因的信息。
在Java 14中,我们可以从堆栈中获取更多的内容,使我们排查问题更加方便。在Java14中,我们将会看到以下的堆栈错误信息:
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 8提供了一个直接的API,可以允许我们程序的部分代码以异步的方式进行运行。让我们看一个例子:
我们假设我们需要调用三个Api接口,然后组合在一起:我们可以依次去调用,如果每一个都需要大概200毫秒,那么获取所有文件的时间将要600毫秒,
如果我们并行运行的话,需要多久?由于现代CPU中大多数都是多核,因此我们可以轻松地同时执行三个Api的调用。使用CompletableFuture
我们可以很容易地实现这一点:
package cn.djc8.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 Expression
Lambda
表达式应该是Java中最强大的功能了,虽然.Net早就有了,它重塑了们编写代码的方式及习惯,Lambda 表达式类似于一个匿名函数,可以接受参数,并且返回,
我们可以将函数赋值给变量,并将其作为参数传递给方法,方法可以返回它,它有一个body
部分,与方法的唯一区别是它不需要定义名称,
表达式将会非常的简明扼要,它通常不需要爱多的格式化代码,让我们看一个例子:
我们希望在某个目录中列出扩展名为.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
"Lambda Expressions are the gateway drug to Java 8, but Streams are the real addiction."
--------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();
}
虽然两种方法返回的结果是一样的,但是哪个更好,不需要 多说了吧?.学习更多关于 stream API的知识. 以上,感谢!
本文来自:【Java】十个需要知道的特性-小码农,转载请保留本条链接,感谢!
- 本文标签: java lambda 经验
- 本文链接: https://djc8.cn/archives/java-ten-features-to-know.html
- 版权声明: 本文由小码农原创发布,转载请遵循《署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)》许可协议授权