21

Java并没有衰落.大家对它的认识才刚刚开始 Java8全新出发-cmdTT的博客

 3 years ago
source link: https://blog.51cto.com/14794073/2499665
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Java并没有衰落.大家对它的认识才刚刚开始
在这里插入图片描述

很高兴能在此给大家分享Java8的新特性。这篇文章将一步一步带你了解Java8的所有新特性。我将通过简单的实例代码向大家展示接口中默认方法,lambda 表达式,方法引用,重复注解的使用. 看过这篇文章之后你将了解流、函数、接口、map扩展、日期中的新变化.不废话,放码过来!

Default Methods for Interfaces(接口中的默认方法)

Java 8准许我们在接口中增加一个通过default关键字修饰的非抽象的方法. 这个特性被我们称为扩展方法. 下面举例说明:

interface Formula {
    double calculate(int a);

    default double sqrt(int a) {
        return Math.sqrt(a);
    }
}

接口Formula 中除了抽象方法calculate 其中还定义了非抽象方法sqrt. 实现类里面继承了抽象方法calculate. 而默认方法可以直接调用.

Formula formula = new Formula() {
    @Override
    public double calculate(int a) {
        return sqrt(a * 100);
    }
};

formula.calculate(100);     // 100.0
formula.sqrt(16);           // 4.0

formula是一个继承Formula 接口的匿名对象. 实例代码十分详细: 仅仅6行代码实现了一个简单的 sqrt(a * 100)计算. 在接下来的章节, 将给大家介绍更加完善的方案实现单个方法对象在Java8中的使用.

Lambda expressions(Lambda表达式)

通过下面实例向大家展示怎么在之前版本中对List中的String排序:

List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");

Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return b.compareTo(a);
    }
});

通过一个静态 Collections.sort 方法接收一个list和compare来对每个List中的元素排序.你经常发现你自己创建一个匿名Comparator类并通过它来实现排序.

为了摆脱通过创建匿名对象的方式来实现排序, Java 8带来了更加简洁的方式实现方式,lambda 表达式:

Collections.sort(names, (String a, String b) -> {
    return b.compareTo(a);
});

你会发现代码更加简洁可读性更强. 甚至它还可以更加简洁:

Collections.sort(names, (String a, String b) -> b.compareTo(a));

对于单行的方法体你可以直接省略{}和return关键字. 甚至可以使它更加简短:

Collections.sort(names, (a, b) -> b.compareTo(a));

Java编译器能够自动识别参数类型所以你能够省略它们. 让我们更深入的了解lambda 表达式在java中更广泛的应用.

Functional Interfaces(函数化接口)

lambda表达式怎么去匹配Java类型呢? 每一个lambda表达式相当于指定一个接口的类型. 一个必须定义一个抽线的方法的函数接口. 每一个lambda表达式类型都将和这个抽象方法匹配.由于默认方法并不是抽象方法,所以你可以自由的添加默认方法到你的函数接口中.

我们能够使用任意接口作为lambda表达式,这个接口中只需要包含一个抽象的方法就可以了. 为了确保你的接口满足需求,你需要在你接口上添加一个@FunctionalInterface注解.当你使用这个注解之后,这个接口只能定义一个抽象方法,否者编译器会报错 .
Example:

@FunctionalInterface
interface Converter<F, T> {
    T convert(F from);
}
Converter<String, Integer> converter = (from) -> Integer.valueOf(from);
Integer converted = converter.convert("123");
System.out.println(converted);    // 123

注意@FunctionalInterface也是有效代码.

Method and Constructor References(方法和构造器的引用)

上面的代码可以通过静态方法应用可以更加简洁:

Converter<String, Integer> converter = Integer::valueOf;
Integer converted = converter.convert("123");
System.out.println(converted);   // 123

Java 8能够传递一个方法或者构造器引用通过 ::关键字. 上面的实例向你们展示了怎么去引用一个静态方法. 我们也能够引用一个普通对象方法:

class Something {
    String startsWith(String s) {
        return String.valueOf(s.charAt(0));
    }
}
Something something = new Something();
Converter<String, String> converter = something::startsWith;
String converted = converter.convert("Java");
System.out.println(converted);    // "J"

让我们来看看::是怎么引用构造器的的. 首先我们定义两个构造器在一个javabean中:

class Person {
    String firstName;
    String lastName;

    Person() {}

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

下一步我们创建一个工厂接口,用它来创建一个Person对象:

interface PersonFactory<P extends Person> {
    P create(String firstName, String lastName);
}

手动实现这个工厂接口, 我们通过构造器和每件事情关联在一起:

PersonFactory<Person> personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");

我们通过 Person::new来创建一个对Person构造器的引用. Java编译器自动的创建一个对象通过PersonFactory.create参数匹配合适的构造器.

Lambda Scopes(Lambda 的作用域)

从 lambda表达式访问局部变量和匿名对象是相似的.你可以访问final修饰的局部变量也能够访问实例化局部属性和静态变量.

Accessing local variables(访问本地变量)

lambda 表达式能够从局部读取一个的final修饰的局部变量:

final int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);

stringConverter.convert(2);     // 3

和匿名对象不同的是,在匿名对象中变量num并不需要用 final修饰. 也能正常调用:

int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);

stringConverter.convert(2);     // 3

然而num必须隐式使用final修饰才能编译. 下面代码就不会编译:

int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);
num = 3;

把num 写到lambda表达式内也是被禁止的.

Accessing fields and static variables(访问属性和静态变量)

与局部变量相比我们在lambda表达式中能够读和写一个全局属性和静态变量. 这和匿名对象中是一样的.

class Lambda4 {
    static int outerStaticNum;
    int outerNum;

    void testScopes() {
        Converter<Integer, String> stringConverter1 = (from) -> {
            outerNum = 23;
            return String.valueOf(from);
        };

        Converter<Integer, String> stringConverter2 = (from) -> {
            outerStaticNum = 72;
            return String.valueOf(from);
        };
    }
}

Accessing Default Interface Methods(访问默认的接口方法)

还记得第一个 formula 的实例嘛? 接口Formula定义一个默认的方法 sqrt,它能够被每个formula实例包括匿名对象访问. 它却不适合lambda 表达式.

lambda表达式不能访问接口的默认方法. 下面代码不能编译通过:

Formula formula = (a) -> sqrt( a * 100);

Built-in Functional Interfaces(内置函数)

DK 1.8 API包含了许多内置函数. 它们中很多在老版本中是大家很熟悉的,像 Comparator 和Runnable. 这些接口通过 添加@FunctionalInterface注解来支持Lambda表达式.

但是Java 8 API 也添加了很多新接口使编程变的更加容易 . 这些新的函数很多是借鉴被大家所熟知的 Google Guava 函数库.即使你很熟悉这些函数库,你也需要关注这些方法是怎么在接口中扩展的以及它们怎么使用.

Predicates( 判断)
判断是传递一个参数返回一个布尔值. 这个接口包含各种默认的方法组成复杂的逻辑判断单元 (and, or, negate)

Predicate<String> predicate = (s) -> s.length() > 0;

predicate.test("foo");              // true
predicate.negate().test("foo");     // false

Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;

Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();

Functions(函数)

函数接收一个参数返回一个结果. 默认方法能够链式调用 (compose, andThen).

Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);

backToString.apply("123");     // "123"

Suppliers(生产者)

Suppliers返回一个给定的泛型类型的结果. 不像函数, Suppliers不需要传递参数.

Supplier<Person> personSupplier = Person::new;
personSupplier.get();   // new Person

Consumers(消费者)
Consumers代表在一个输入参数上执行一项操作.

Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));

Comparators(比较)
Comparators在老版本中大家都比较熟悉. Java 8 为这个接口增加了几种默认的方法.

Comparator<Person> comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName);

Person p1 = new Person("John", "Doe");
Person p2 = new Person("Alice", "Wonderland");

comparator.compare(p1, p2);             // > 0
comparator.reversed().compare(p1, p2);  // < 0

Optionals(选项)
Optionals 并不是一个函数接口, 相反的它的作用就是避免出现空指针异常.它是一个很重要的概念对以一部分,然我们来快速了解它.

Optional是一个简单的容器里面包含有空或者非空的值.想象一下如果有一个方法能够返回一个空或者一个非空的值 . 在Java8中你可以通过返回一个Optional来替代空.

Optional<String> optional = Optional.of("bam");

optional.isPresent();           // true
optional.get();                 // "bam"
optional.orElse("fallback");    // "bam"

optional.ifPresent((s) -> System.out.println(s.charAt(0)));     // "b"

Streams
java.util.Stream代表一个元素序列,在它上面能够进行一种或多种操作. Stream操作可以包括对中间部分或者终端部分进行操作. 终端操作能够返回一个确定的类型, 中间操作能返回流的本身你能够通过多种方法调用某一行. Streams 可以通过一个源来创建, 例如. 像 java.util.Collection中的lists 或 sets (maps 不支持)都可以创建流. Stream 能够串行操作也能并行操作.

首先看下流怎么进行序列操作. 我们先通过一个包含字符串List创建源:

List<String> stringCollection = new ArrayList<>();
stringCollection.add("ddd2");
stringCollection.add("aaa2");
stringCollection.add("bbb1");
stringCollection.add("aaa1");
stringCollection.add("bbb3");
stringCollection.add("ccc");
stringCollection.add("bbb2");
stringCollection.add("ddd1");

在Java8中Collections已经继承了streams ,所以你能够通过in Java 8 are extended so you can simply create 调用Collection.stream() 和 Collection.parallelStream()来创建流.下面将给你介绍大部分流的操作 .

Filter(过滤)
Filter接收一个判断用来过滤流中的所有元素. 这个操作是中间操作,它能够使我们对结果进行另一个流操作(forEach) . ForEach接受一个consumer操作对每一个过滤的流元素中. ForEach是一个终端操作. 它返回值void,所以我们不能调用另一个函数操作.

stringCollection
    .stream()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);

// "aaa2", "aaa1"

Sorted(排序)
Sorted是一个中间操作,它能够返回一个排序的流.这个元素按照自然排序除非你传递一个Comparator .

stringCollection
    .stream()
    .sorted()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);

// "aaa1", "aaa2"

注意 sorted 仅仅是创建一个排序后的视图操作,并没有操作排序返回的集合. 排序的 stringCollection并没有受到影响:

System.out.println(stringCollection);
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1

Map(映射)
中间操作 map转换每个元素到另一个元素中通过给定的函数.下面的实例是把把每一个字符串转换成大写.你也可以使用map转换每个对象的类型. 泛型流依赖你传递给map的泛型函数.

stringCollection
    .stream()
    .map(String::toUpperCase)
    .sorted((a, b) -> b.compareTo(a))
    .forEach(System.out::println);

// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"

Match(匹配)
可以使用各种匹配操作来确定是否是确定的流. 所有这些操作是终端操作并返回一个布尔值.

boolean anyStartsWithA =
    stringCollection
        .stream()
        .anyMatch((s) -> s.startsWith("a"));

System.out.println(anyStartsWithA);      // true

boolean allStartsWithA =
    stringCollection
        .stream()
        .allMatch((s) -> s.startsWith("a"));

System.out.println(allStartsWithA);      // false

boolean noneStartsWithZ =
    stringCollection
        .stream()
        .noneMatch((s) -> s.startsWith("z"));

System.out.println(noneStartsWithZ);      // true

Count(统计)
Count是一个终端操作返回流中元素的个数.

long startsWithB =
    stringCollection
        .stream()
        .filter((s) -> s.startsWith("b"))
        .count();

System.out.println(startsWithB);    // 3
Reduce(合并)

这个终端操作完成一个流中元素合并操作通过给定的函数.返回的结果通过 Optional来保存值.

Optional<String> reduced =
    stringCollection
        .stream()
        .sorted()
        .reduce((s1, s2) -> s1 + "#" + s2);

reduced.ifPresent(System.out::println);
// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"
Parallel Streams(并行流)

上文提到过流可以是串行的也可以是并行的. 串行的序列操作是单线程的,而并行流是在多个线程上进行操作.

下面的实例向你展示通过并行流增加性能.

首先创建一个元素出现次数唯一的大的List:

int max = 1000000;
List<String> values = new ArrayList<>(max);
for (int i = 0; i < max; i++) {
    UUID uuid = UUID.randomUUID();
    values.add(uuid.toString());
}

现在测试它的排序所消耗的时间.

Sequential Sort(串行排序)
long t0 = System.nanoTime();

long count = values.stream().sorted().count();
System.out.println(count);

long t1 = System.nanoTime();

long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
System.out.println(String.format("sequential sort took: %d ms", millis));

// sequential sort took: 899 ms
Parallel Sort(并行排序)
long t0 = System.nanoTime();

long count = values.parallelStream().sorted().count();
System.out.println(count);

long t1 = System.nanoTime();

long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
System.out.println(String.format("parallel sort took: %d ms", millis));

// parallel sort took: 472 ms

你能看到上述代码基本相同的,但是并行大约快了50%.你可以测试着用 parallelStream()操作代替stream().

Map(映射)
上文提到map并不支持流操作. 但是新的map支持各种新方法和常见的任务操作.

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

for (int i = 0; i < 10; i++) {
    map.putIfAbsent(i, "val" + i);
}

map.forEach((id, val) -> System.out.println(val));

上面的代码简洁易懂: putIfAbsent方法对传统的非空检验 ; forEach 方法遍历map中每一个值进行consumer操作.

这个实例展示了怎么利用函数在map上进行操作:

map.computeIfPresent(3, (num, val) -> val + num);
map.get(3);             // val33

map.computeIfPresent(9, (num, val) -> null);
map.containsKey(9);     // false

map.computeIfAbsent(23, num -> "val" + num);
map.containsKey(23);    // true

map.computeIfAbsent(3, num -> "bam");
map.get(3);             // val33

下一步,我们学习怎么给定一个key怎么移除对象操作, 现在加入给一个键值对:

map.remove(3, "val3");
map.get(3);             // val33

map.remove(3, "val33");
map.get(3);             // null

另一个有用的方法:

map.getOrDefault(42, "not found");  // not found

map中合并对象操作很容易:

map.merge(9, "val9", (value, newValue) -> value.concat(newValue));
map.get(9);             // val9

map.merge(9, "concat", (value, newValue) -> value.concat(newValue));
map.get(9);             // val9concat

合并操作如果键值对不存在就添加到里面, 合并操作也可以用来改变存在的值.

Date API(日期API)

Java 8 包含一个全新的日期和时间操作API在 java.time包里面. 新的Date API 可以和 Joda-Time 包相媲美, 不过,它们并不一样. 下面给你实例一下新日期里面主要新的API.

Clock(时钟)

时钟提供了访问现在的日期和时间的方法. 时钟意思到时区将可能替代 System.currentTimeMillis() 来获取现在的毫秒数. 这样一个瞬时点在时间线上用类 Instant来代表.代替能够被使用的java.util.Date 传统对象.

Clock clock = Clock.systemDefaultZone();
long millis = clock.millis();

Instant instant = clock.instant();
Date legacyDate = Date.from(instant);   // legacy java.util.Date

Timezones(时区)
时区被ZoneId代替. 你可以很方便的访问通过一个静态的工厂方法. 时区定义一个弥补了一个重要在瞬时和本地时间和日期上的转换.

System.out.println(ZoneId.getAvailableZoneIds());
// prints all available timezone ids

ZoneId zone1 = ZoneId.of("Europe/Berlin");
ZoneId zone2 = ZoneId.of("Brazil/East");
System.out.println(zone1.getRules());
System.out.println(zone2.getRules());

// ZoneRules[currentStandardOffset=+01:00]
// ZoneRules[currentStandardOffset=-03:00]
LocalTime(本地时间)

本地时间代表一个不包含时区的时间, 例如. 10pm 或者 17:30:15. 下面两个实例创建两个本地时间包含上面定义的时区. 然后我们比较两个时间和日历在小时和分钟的不同之处.

LocalTime now1 = LocalTime.now(zone1);
LocalTime now2 = LocalTime.now(zone2);

System.out.println(now1.isBefore(now2));  // false

long hoursBetween = ChronoUnit.HOURS.between(now1, now2);
long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);

System.out.println(hoursBetween);       // -3
System.out.println(minutesBetween);     // -239

LocalTime可以从各种工厂方法简单的创建新实例, 包含格式化的字符串.

LocalTime late = LocalTime.of(23, 59, 59);
System.out.println(late);       // 23:59:59

DateTimeFormatter germanFormatter =
    DateTimeFormatter
        .ofLocalizedTime(FormatStyle.SHORT)
        .withLocale(Locale.GERMAN);

LocalTime leetTime = LocalTime.parse("13:37", germanFormatter);
System.out.println(leetTime);   // 13:37

LocalDate(本地日期)
本地日期代表了清晰的日期 , 例如. 2014-03-11. 它是不可更改的完全模拟LocalTime. 这个简单的实例展示怎么计算新的日期通过增加或者减去天、月、或者年. 注意每个操纵返回一个新的实例.

LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
LocalDate yesterday = tomorrow.minusDays(2);

LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4);
DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();
System.out.println(dayOfWeek);    // FRIDAY

通过一个字符串解析LocalDate from 和解析LocalTime一样简单:

DateTimeFormatter germanFormatter =
    DateTimeFormatter
        .ofLocalizedDate(FormatStyle.MEDIUM)
        .withLocale(Locale.GERMAN);

LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter);
System.out.println(xmas);   // 2014-12-24

LocalDateTime(本地日期时间)
LocalDateTime代表一个日期时间. 它是上例时间和日期的一个组合. LocalDateTime也是不可变的,它工作原理和 LocalTime、LocalDate工作原理一样.我们能够利用方法获取一个属性从date-time:

LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59);

DayOfWeek dayOfWeek = sylvester.getDayOfWeek();
System.out.println(dayOfWeek);      // WEDNESDAY

Month month = sylvester.getMonth();
System.out.println(month);          // DECEMBER

long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY);
System.out.println(minuteOfDay);    // 1439

传统的 timezone 能够转换成一个 instant. Instants 也能够很轻松转换成一个传统的java.util.Date.

Instant instant = sylvester
        .atZone(ZoneId.systemDefault())
        .toInstant();

Date legacyDate = Date.from(instant);
System.out.println(legacyDate);     // Wed Dec 31 23:59:59 CET 2014

格式化date-times 就是格式化dates 和 times一样.我们也可以使用预先定义的格式才创建定制的格式 .

DateTimeFormatter formatter =
    DateTimeFormatter
        .ofPattern("MMM dd, yyyy - HH:mm");

LocalDateTime parsed = LocalDateTime.parse("Nov 03, 2014 - 07:13", formatter);
String string = formatter.format(parsed);
System.out.println(string);     // Nov 03, 2014 - 07:13

不像java.text.NumberFormat 新的 DateTimeFormatter是不可修改的且线程安全的.

Annotations(注解)

注解在Java 8中可重复使用.让我们通过实例来深入了解.

首先, 我们定义一个包装注解,它包含一个实际的注解数组:

@interface Hints {
    Hint[] value();
}

@Repeatable(Hints.class)
@interface Hint {
    String value();
}

Java 8使我们能够使用一样的注解通过@Repeatable注解定义.

Variant 1: 使用容器注解(old school)
@Hints({@Hint("hint1"), @Hint("hint2")})
class Person {}
Variant 2: 使用重复注解(new school)
@Hint("hint1")
@Hint("hint2")
class Person {}

使用variant 2java编译器会隐式的设置 一个@Hints注解. 这个是很重要的通过反射读取注解信息.

Hint hint = Person.class.getAnnotation(Hint.class);
System.out.println(hint);                   // null

Hints hints1 = Person.class.getAnnotation(Hints.class);
System.out.println(hints1.value().length);  // 2

Hint[] hints2 = Person.class.getAnnotationsByType(Hint.class);
System.out.println(hints2.length);          // 2

虽然我们从来没有定义 @Hints注解在 Person 类上,它一直能获取到通过 getAnnotation(Hints.class). 然而,这里有更方便的方法getAnnotationsByType它能够直接访问所有的 @Hint 注解.

此外使用Java8注解是扩展的两个新的目标:

@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@interface MyAnnotation {}

我的Java 8新特征实例到此结束. 如果你想学习 JDK 8 API所有新特性和类, 可以关注公众号:程序零世界 它帮你了解所有的新类和一些隐藏的新特性在 JDK 8,像 Arrays.parallelSort, StampedLock 和CompletableFuture等等.

我希望我的这篇文章能够帮助你,同时也希望你读后有所收获.你也可以通过公众号:程序零世界 向我反馈意见.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK