原创

【Java】十个需要知道的特性

本文,将会介绍Java中常用的十种编程的特性。

集合工厂方法

集合是开发中常用的功能。它们像是一个容器,我们将数据存储在这个容器中。

集合还可以用于排序,搜索以及迭代循环,这种特性让开发人员工作更轻松。Java中,提供了集中基本的接口,比如:ListSetMap等等。

对于许多人来说,创建 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;
};

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) {
}

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这不在程序设计的预期内。

另一方面,如果方法签名不返回签名指定的返回值,代码将会很混乱,看下面的代码,OptionalShow 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】十个需要知道的特性-小码农,转载请保留本条链接,感谢!

温馨提示:
本文最后更新于 2023年01月20日,已超过 683 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我
正文到此结束
本文目录