Java新特性 - Jdk8版本

 1.Jdk8新特性总览

 

2.日期相关API

2.1 旧版日期时间 API 存在的问题

1. 设计很差: 在 java.util java.sql 的包中都有日期类, java.util.Date 同时包含日期和时间,而 java.sql.Date 仅包 含日期。此外用于格式化和解析的类在 java.text 包中定义。
2. 非线程安全: java.util.Date 是非线程安全的,所有的日期类都是可变的,这是 Java 日期类最大的问题之一。
3. 时区处理麻烦:日期类并不提供国际化,没有时区支持,因此 Java 引入了 java.util.Calendar java.util.TimeZone 类,但他们同样存在上述所有的问题。

2.2新日期时间 API介绍

JDK 8 中增加了一套全新的日期时间 API ,这套 API 设计合理,是线程安全的。新的日期及时间 API 位于 java.time 中,下面是一些关键类。
LocalDate :表示日期,包含年月日,格式为 2019-10-16
LocalTime :表示时间,包含时分秒,格式为 16:38:54.158549300
LocalDateTime :表示日期时间,包含年月日,时分秒,格式为 2018-09-06T15:33:56.750
DateTimeFormatter :日期时间格式化类。
Instant :时间戳,表示一个特定的时间瞬间。
Duration :用于计算 2 个时间 (LocalTime ,时分秒 ) 的距离
Period :用于计算 2 个日期 (LocalDate ,年月日 ) 的距离
ZonedDateTime :包含时区的时间
Java 中使用的历法是 ISO 8601 日历系统,它是世界民用历法,也就是我们所说的公历。平年有 365 天,闰年是 366 天。此外 Java 8 还提供了 4 套其他历法。

JDK 8的日期和时间类

LocalDateLocalTimeLocalDateTime类的实例是不可变的对象,分别表示使用 ISO-8601 日历系统的日期、时间、日期和时间。它们提供了简单的日期或时间,并不包含当前的时间信息,也不包含与时区相关的信息。

// LocalDate:获取日期时间的信息。格式为 2019-10-16
@Test
public void test01() {
// 创建指定日期
LocalDate fj = LocalDate.of(1985, 9, 23);
System.out.println("fj = " + fj); // 1985-09-23
// 得到当前日期
LocalDate nowDate = LocalDate.now();
System.out.println("nowDate = " + nowDate); // 2019-10-16
// 获取日期信息
System.out.println("年: " + nowDate.getYear());
System.out.println("月: " + nowDate.getMonthValue());
System.out.println("日: " + nowDate.getDayOfMonth());
System.out.println("星期: " + nowDate.getDayOfWeek());
}
// LocalTime类: 获取时间信息。格式为 16:38:54.158549300
@Test
public void test02() {
// 得到指定的时间
LocalTime time = LocalTime.of(12,15, 28, 129_900_000);
System.out.println("time = " + time);
// 得到当前时间
LocalTime nowTime = LocalTime.now();
System.out.println("nowTime = " + nowTime);
// 获取时间信息
System.out.println("小时: " + nowTime.getHour());
System.out.println("分钟: " + nowTime.getMinute());
System.out.println("秒: " + nowTime.getSecond());
System.out.println("纳秒: " + nowTime.getNano());
}
// LocalDateTime类: 获取日期时间信息。格式为 2018-09-06T15:33:56.750
@Test
public void test03() {
LocalDateTime fj = LocalDateTime.of(1985, 9, 23, 9, 10, 20);
System.out.println("fj = " + fj); // 1985-09-23T09:10:20
// 得到当前日期时间
LocalDateTime now = LocalDateTime.now();
System.out.println("now = " + now); // 2019-10-16T16:42:24.497896800
System.out.println(now.getYear());
System.out.println(now.getMonthValue());
System.out.println(now.getDayOfMonth());
System.out.println(now.getHour());
System.out.println(now.getMinute()); 
System.out.println(now.getSecond());
System.out.println(now.getNano());
 }
 
对日期时间的修改,对已存在的 LocalDate 对象,创建它的修改版,最简单的方式是使用 withAttribute 方法。 withAttribute 方法会创建对象的一个副本,并按照需要修改它的属性。以下所有的方法都返回了一个修改属性的对 象,他们不会影响原来的对象。
// LocalDateTime类: 对日期时间的修改
@Test
public void test05() {
LocalDateTime now = LocalDateTime.now();
System.out.println("now = " + now);
// 修改日期时间
LocalDateTime setYear = now.withYear(2078);
System.out.println("修改年份: " + setYear);
System.out.println("now == setYear: " + (now == setYear));
System.out.println("修改月份: " + now.withMonth(6));
System.out.println("修改小时: " + now.withHour(9));
System.out.println("修改分钟: " + now.withMinute(11));
// 再当前对象的基础上加上或减去指定的时间
LocalDateTime localDateTime = now.plusDays(5);
System.out.println("5天后: " + localDateTime);
System.out.println("now == localDateTime: " + (now == localDateTime));
System.out.println("10年后: " + now.plusYears(10));
System.out.println("20月后: " + now.plusMonths(20));
System.out.println("20年前: " + now.minusYears(20));
System.out.println("5月前: " + now.minusMonths(5));
System.out.println("100天前: " + now.minusDays(100));
}
 
日期时间的比较

@Test
public void test06() {
// 在JDK8中,LocalDate类中使用isBefore()、isAfter()、equals()方法来比较两个日期,可直接进行比较。
LocalDate now = LocalDate.now();
LocalDate date = LocalDate.of(2018, 8, 8);
System.out.println(now.isBefore(date)); // false
System.out.println(now.isAfter(date)); // true
}

通过 java.time.format.DateTimeFormatter 类可以进行日期时间解析与格式化。

@Test
public void test04() {
// 得到当前日期时间
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 将日期时间格式化为字符串
String format = now.format(formatter);
System.out.println("format = " + format);
// 将字符串解析为日期时间
LocalDateTime parse = LocalDateTime.parse("1985-09-23 10:12:22", formatter);
System.out.println("parse = " + parse);
}
 
Instant 时间戳 / 时间线,内部保存了从 1970 1 1 00:00:00 以来的秒和纳秒。
// 时间戳
@Test
public void test07() {
Instant now = Instant.now();
System.out.println("当前时间戳 = " + now);
// 获取从1970年1月1日 00:00:00的秒
System.out.println(now.getNano());
System.out.println(now.getEpochSecond());
System.out.println(now.toEpochMilli());
System.out.println(System.currentTimeMillis());
Instant instant = Instant.ofEpochSecond(5);
System.out.println(instant);
}

Duration/Period: 计算日期时间差。

1. Duration :用于计算 2 个时间 (LocalTime ,时分秒 ) 的距离
2. Period :用于计算 2 个日期 (LocalDate ,年月日 ) 的距离

// Duration/Period类: 计算日期时间差
@Test
public void test08() {
// Duration计算时间的距离
LocalTime now = LocalTime.now();
LocalTime time = LocalTime.of(14, 15, 20);
Duration duration = Duration.between(time, now);
System.out.println("相差的天数:" + duration.toDays());
System.out.println("相差的小时数:" + duration.toHours());
System.out.println("相差的分钟数:" + duration.toMinutes());
System.out.println("相差的秒数:" + duration.toSeconds());
// Period计算日期的距离
LocalDate nowDate = LocalDate.now();
LocalDate date = LocalDate.of(1998, 8, 8);
// 让后面的时间减去前面的时间
Period period = Period.between(date, nowDate);
System.out.println("相差的年:" + period.getYears());
System.out.println("相差的月:" + period.getMonths());
System.out.println("相差的天:" + period.getDays());
}
 
有时我们可能需要获取例如:将日期调整到 下一个月的第一天 等操作。可以通过时间校正器来进行。
TemporalAdjuster : 时间校正器。
TemporalAdjusters : 该类通过静态方法提供了大量的常用 TemporalAdjuster 的实现。
// TemporalAdjuster类:自定义调整时间
@Test
public void test09() {
LocalDateTime now = LocalDateTime.now();
// 得到下一个月的第一天
TemporalAdjuster firsWeekDayOfNextMonth = temporal -> {
LocalDateTime dateTime = (LocalDateTime) temporal;
LocalDateTime nextMonth = dateTime.plusMonths(1).withDayOfMonth(1);
System.out.println("nextMonth = " + nextMonth);
return nextMonth;
};
LocalDateTime nextMonth = now.with(firsWeekDayOfNextMonth);
System.out.println("nextMonth = " + nextMonth);
}
 
Java8 中加入了对时区的支持, LocalDate LocalTime LocalDateTime 是不带时区的,带时区的日期时间类分别 为: ZonedDate ZonedTime ZonedDateTime 其中每个时区都对应着 ID ID 的格式为 区域 / 城市 。例如 : Asia/Shanghai 等。 ZoneId :该类中包含了所有的时区信息。
// 设置日期时间的时区
@Test
public void test10() {
// 1.获取所有的时区ID
// ZoneId.getAvailableZoneIds().forEach(System.out::println);
// 不带时间,获取计算机的当前时间
LocalDateTime now = LocalDateTime.now(); // 中国使用的东八区的时区.比标准时间早8个小时
System.out.println("now = " + now);
// 2.操作带时区的类
// now(Clock.systemUTC()): 创建世界标准时间
ZonedDateTime bz = ZonedDateTime.now(Clock.systemUTC());
System.out.println("bz = " + bz);
// now(): 使用计算机的默认的时区,创建日期时间
ZonedDateTime now1 = ZonedDateTime.now();
System.out.println("now1 = " + now1); // 2019-10-19T16:19:44.007153500+08:00[Asia/Shanghai]
// 使用指定的时区创建日期时间
ZonedDateTime now2 = ZonedDateTime.now(ZoneId.of("America/Vancouver"));
System.out.println("now2 = " + now2); // 2019-10-19T01:21:44.248794200-07:00[America/Vancouver]
}
 
小结
详细学习了新的日期是时间相关类, LocalDate 表示日期 , 包含年月日 ,LocalTime 表示时间 , 包含时分
,LocalDateTime = LocalDate + LocalTime, 时间的格式化和解析 , 通过 DateTimeFormatter 类型进行 .
学习了 Instant , 方便操作秒和纳秒 , 一般是给程序使用的 . 学习 Duration/Period 计算日期或时间的距离 , 还使用时间调
整器方便的调整时间 , 学习了带时区的 3 个类 ZoneDate/ZoneTime/ZoneDateTime
JDK 8 新的日期和时间 API 的优势:
1. 新版的日期和时间 API 中,日期和时间对象是不可变的。操纵的日期不会影响老值,而是新生成一个实例。
2. 新的 API 提供了两种不同的时间表示方式,有效地区分了人和机器的不同需求。
3. TemporalAdjuster 可以更精确的操纵日期,还可以自定义日期调整器。
4. 是线程安全的
 

3.重复注解的使用

自从Java 5中引入 注解 以来,注解开始变得非常流行,并在各个框架和项目中被广泛使用。不过注解有一个很大的限制是:在同一个地方不能多次使用同一个注解。JDK 8引入了重复注解的概念,允许在同一个地方多次使用同一个注解。在JDK 8中使用@Repeatable注解定义重复注解。

重复注解的使用步骤:
1. 定义重复的注解容器注解
@Retention(RetentionPolicy.RUNTIME)
@interface MyTests {
 MyTest[] value();
}
2. 定义一个可以重复的注解
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(MyTests.class)
@interface MyTest {
String value();
}
3. 配置多个重复的注解
@MyTest("tbc")
@MyTest("tba")
@MyTest("tba")
public class Demo01 {
  @MyTest("mbc")
  @MyTest("mba")
  public void test() throws NoSuchMethodException {
  }
}
4. 解析得到指定注解
@MyTest("tbc")
@MyTest("tba")
@MyTest("tba")
public class Demo01 {
    @Test
    @MyTest("mbc")
    @MyTest("mba")
    public void test() throws NoSuchMethodException {
// 4.解析得到类上的指定注解
        MyTest[] tests = Demo01.class.getAnnotationsByType(MyTest.class);
        for (MyTest test : tests) {
            System.out.println(test.value());
        }
// 得到方法上的指定注解
        Annotation[] tests1 = Demo01.class.getMethod("test").getAnnotationsByType(MyTest.class);
        for (Annotation annotation : tests1) {
            System.out.println("annotation = " + annotation);
        }
    }
}

类型注解的使用

JDK 8@Target元注解新增了两种类型: TYPE_PARAMETER TYPE_USE TYPE_PARAMETER :表示该注解能写在类型参数的声明语句中。 类型参数声明如: <T> TYPE_USE :表示注解可以再任何用到类型的地方使用。

TYPE_PARAMETER的使用

@Target(ElementType.TYPE_PARAMETER) 
@interface TyptParam {
 }

public class Demo02<@TyptParam T> {

    public static void main( String[] args) {
    }
    public <@TyptParam E> void test( String a) {
    }
}

TYPE_USE的使用

// 3.配置多个重复的注解
@Target(ElementType.TYPE_USE)
@interface NotNull {
}

public class Demo02<@TyptParam T extends String> {
    private @NotNull int a = 10;
    public static void main(@NotNull String[] args) {
        @NotNull int x = 1;
        @NotNull String s = new @NotNull String();
    }
    public <@TyptParam E> void test( String a) {
    }
}

小结

通过 @Repeatable 元注解可以定义可重复注解, TYPE_PARAMETER 可以让注解放在泛型上, TYPE_USE 可以让注解放 在类型的前面 .

4.Java8反射获取形参名称

    曾经有个项目希望实现一个这样的技术需求:从方法中获取形参的真实名称,然后转成某个xml的一个属性。当时本以为很简单,可以直接通过API获取,但是通过反射获取的形参的名称总是:arg0这样的参数。项目在当时使用的JDK7,所以强行给每个形参加上注解指明方法的形参,类似:

    public void execute(@param("taskId") String taskId) {
            System.out.println(taskId);
        }

相信大家也看到了,这个做法确实不够优雅,但是在当时应该是唯一的解决办法了。

假设有如下方法:

    class Executor {
        public void execute(String taskId) {
            System.out.println(taskId);
        }
    }

我需要从execute方法中获取参数taskId这个形参的名称:taskId在Java8中需要进行环境变量配置才可以通过如下代码获取:

   public static void main(String[] args) throws NoSuchMethodException {
        Class<Executor> executorClass = Executor.class;
        Method method = executorClass.getMethod("execute", String.class);
        for (Parameter parameter : method.getParameters()) {
            String name = parameter.getName();
            System.out.println(name);
        }
    }

配置

5.Java Core 相关

Map 新增

public static void main(String[] arg) {
    Map map = new HashMap();
    //如果key不存在返回默认值
    System.out.println(map.getOrDefault("a", 123));
    map.put("a", 123);
    //lambda结果放入该key
    map.compute("a", (key, value) -> key + "" + value);
    System.out.println(map.get("a"));//a123
    //如果key不存在,则将lambda结果放入该key
    map.computeIfAbsent("a", (key) -> key + "234");
    //如果key存在,则将lambda结果更新该key,如果lambda结果返回null,则移除该key
    map.computeIfPresent("a", (key, value) -> key + "kv" + value);//->akva123
    //如果key对应的值为null则用新value更新,否则用lambda结果更新,如果lambda的值为null,则移除此key
    //lambda表达式中参数oldvalue是原key对应的value,newvalue是第二个参数指定的值
    map.merge("a", "newvalue", (oldvalue, newvalue) -> oldvalue + "dv" + newvalue);//->akva123dvnewvalue
    //如果key不存在,则将值放入此key
    map.putIfAbsent("b", "b123");
    //key存在并且key对应的value等于新的值时,移除此key
    map.remove("b", "b124");
    //如果存在key,则替换key的值
    map.replace("b", "b125");
    //遍历
    map.forEach((key, value) -> System.out.println("key:" + key + ";value:" + value));

处理数值

Java8添加了对无符号数的额外支持。Java中的数值总是有符号的,例如,让我们来观察Integer:int可表示最多2 ** 32个数。Java中的数值默认为有符号的,所以最后一个二进制数字表示符号(0为正数,1为负数)。所以从十进制的0开始,最大的有符号正整数为2 ** 31 - 1。

你可以通过Integer.MAX_VALUE来访问它:

System.out.println(Integer.MAX_VALUE);      // 2147483647
System.out.println(Integer.MAX_VALUE + 1);  // -2147483648
Java8添加了解析无符号整数的支持,让我们看看它如何工作:
long maxUnsignedInt = (1l << 32) - 1;
String string = String.valueOf(maxUnsignedInt);
int unsignedInt = Integer.parseUnsignedInt(string, 10);
String string2 = Integer.toUnsignedString(unsignedInt, 10);

就像你看到的那样,现在可以将最大的无符号数2 ** 32 - 1解析为整数。而且你也可以将这个数值转换回无符号数的字符串表示。这在之前不可能使用parseInt完成,就像这个例子展示的那样:

try {
    Integer.parseInt(string, 10);
}
catch (NumberFormatException e) {
    System.err.println("could not parse signed int of " + maxUnsignedInt);
}

这个数值不可解析为有符号整数,因为它超出了最大范围2 ** 31 - 1。 算术运算Math工具类新增了一些方法来处理数值溢出。这是什么意思呢? 我们已经看到了所有数值类型都有最大值。所以当算术运算的结果不能被它的大小装下时,会发生什么呢?

System.out.println(Integer.MAX_VALUE);      // 2147483647
System.out.println(Integer.MAX_VALUE + 1);  // -2147483648

就像你看到的那样,发生了整数溢出,这通常是我们不愿意看到的。Java8添加了严格数学运算的支持来解决这个问题。Math扩展了一些方法,它们全部以exact结尾,例如addExact。当运算结果不能被数值类型装下时,这些方法通过抛出ArithmeticException异常来合理地处理溢出。

try {
    Math.addExact(Integer.MAX_VALUE, 1);
}
catch (ArithmeticException e) {
    System.err.println(e.getMessage());
    // => integer overflow
}

当尝试通过toIntExact将长整数转换为整数时,可能会抛出同样的异常:

try {
    Math.toIntExact(Long.MAX_VALUE);
}
catch (ArithmeticException e) {
    System.err.println(e.getMessage());
    // => integer overflow
}

处理文件

Files工具类首次在Java7中引入,作为NIO的一部分。JDK8 API添加了一些额外的方法,它们可以将文件用于函数式数据流。让我们深入探索一些代码示例。列出文件Files.list方法将指定目录的所有路径转换为数据流,便于我们在文件系统的内容上使用类似filter和sorted的流操作。

try (Stream<Path> stream = Files.list(Paths.get(""))) {
    String joined = stream
        .map(String::valueOf)
        .filter(path -> !path.startsWith("."))
        .sorted()
        .collect(Collectors.joining("; "));
    System.out.println("List: " + joined);
}

上面的例子列出了当前工作目录的所有文件,之后将每个路径都映射为它的字符串表示。之后结果被过滤、排序,最后连接为一个字符串。如果你还不熟悉函数式数据流,你应该阅读我的Java8数据流教程。你可能已经注意到,数据流的创建包装在try-with语句中。数据流实现了AutoCloseable,并且这里我们需要显式关闭数据流,因为它基于IO操作。返回的数据流是DirectoryStream的封装。如果需要及时处理文件资源,就应该使用try-with结构来确保在流式操作完成后,数据流的close方法被调用。
 查找文件

下面的例子演示了如何查找在目录及其子目录下的文件:

Path start = Paths.get("");
int maxDepth = 5;
try (Stream<Path> stream = Files.find(start, maxDepth, (path, attr) ->
        String.valueOf(path).endsWith(".js"))) {
    String joined = stream
        .sorted()
        .map(String::valueOf)
        .collect(Collectors.joining("; "));
    System.out.println("Found: " + joined);
}
find方法接受三个参数: 目录路径start是起始点,maxDepth定义了最大搜索深度。第三个参数是一个匹配谓词,定义了搜索的逻辑。上面的例子中,我们搜索了所有JavaScirpt文件(以.js结尾的文件名)。我们可以使用Files.walk方法来完成相同的行为。这个方法会遍历每个文件,而不需要传递搜索谓词。
Path start = Paths.get("");
int maxDepth = 5;
try (Stream<Path> stream = Files.walk(start, maxDepth)) {
    String joined = stream
        .map(String::valueOf)
        .filter(path -> path.endsWith(".js"))
        .sorted()
        .collect(Collectors.joining("; "));
    System.out.println("walk(): " + joined);
}
这个例子中,我们使用了流式操作filter来完成和上个例子相同的行为。
读写文件

将文本文件读到内存,以及向文本文件写入字符串在Java 8 中是简单的任务。不需要再去摆弄读写器了。Files.readAllLines从指定的文件把所有行读进字符串列表中。你可以简单地修改这个列表,并且将它通过Files.write写到另一个文件中:

List<String> lines = Files.readAllLines(Paths.get("res/nashorn1.js"));
lines.add("print('foobar');");
Files.write(Paths.get("res/nashorn1-modified.js"), lines);

要注意这些方法对内存并不十分高效,因为整个文件都会读进内存。文件越大,所用的堆区也就越大。你可以使用Files.lines方法来作为内存高效的替代。这个方法读取每一行,并使用函数式数据流来对其流式处理,而不是一次性把所有行都读进内存。

try (Stream<String> stream = Files.lines(Paths.get("res/nashorn1.js"))) {
    stream
        .filter(line -> line.contains("print"))
        .map(String::trim)
        .forEach(System.out::println);
}
如果你需要更多的精细控制,你需要构造一个新的BufferedReader来代替:
Path path = Paths.get("res/nashorn1.js");
try (BufferedReader reader = Files.newBufferedReader(path)) {
    System.out.println(reader.readLine());
}
或者,你需要写入文件时,简单地构造一个BufferedWriter来代替:
Path path = Paths.get("res/output.js");
try (BufferedWriter writer = Files.newBufferedWriter(path)) {
    writer.write("print('Hello World');");
}

BufferedReader也可以访问函数式数据流。lines方法在它所有行上面构建数据流:
Path path = Paths.get("res/nashorn1.js");
try (BufferedReader reader = Files.newBufferedReader(path)) {
    long countPrints = reader
        .lines()
        .filter(line -> line.contains("print"))
        .count();
    System.out.println(countPrints);
}
目前为止你可以看到Java8提供了三个简单的方法来读取文本文件的每一行,使文件处理更加便捷。

不幸的是你需要显式使用try-with语句来关闭文件流,这会使示例代码有些凌乱。我期待函数式数据流可以在调用类似count和collect时可以自动关闭,因为你不能在相同数据流上调用终止操作两次。

 java.util.Random

在Java8中java.util.Random类的一个非常明显的变化就是新增了返回随机数流(random Stream of numbers)的一些方法。

下面的代码是创建一个无穷尽的double类型的数字流,这些数字在0(包括0)和1(不包含1)之间。

Random random = new Random();
DoubleStream doubleStream = random.doubles();

下面的代码是创建一个无穷尽的int类型的数字流,这些数字在0(包括0)和100(不包括100)之间。

Random random = new Random();
IntStream intStream = random.ints(0, 100);
那么这些无穷尽的数字流用来做什么呢? 接下来,我通过一些案例来分析。记住,这些无穷大的数字流只能通过某种方式被截断(limited)。示例1: 创建10个随机的整数流并打印出来:
intStream.limit(10).forEach(System.out::println);

示例2: 创建100个随机整数:

    List<Integer> randomBetween0And99 = intStream
                                       .limit(100)
                                       .boxed()
                                       .collect(Collectors.toList());

对于高斯伪随机数(gaussian pseudo-random values)来说,random.doubles()方法所创建的流不能等价于高斯伪随机数,然而,如果用java8所提供的功能是非常容易实现的。

Random random = new Random();
DoubleStream gaussianStream = Stream.generate(random::nextGaussian).mapToDouble(e -> e);
这里,我使用了Stream.generate api,并传入Supplier 类的对象作为参数,这个对象是通过调用Random类中的方法 nextGaussian()创建另一个高斯伪随机数。接下来,我们来对double类型的伪随机数流和double类型的高斯伪随机数流做一个更加有意思的事情,那就是获得两个流的随机数的分配情况。预期的结果是: double类型的伪随机数是均匀的分配的,而double类型的高斯伪随机数应该是正态分布的。通过下面的代码,我生成了一百万个伪随机数,这是通过java8提供的api实现的:
Random random = new Random();
DoubleStream doubleStream = random.doubles(-1.0, 1.0);
LinkedHashMap<Range, Integer> rangeCountMap = doubleStream.limit(1000000)
    .boxed()
    .map(Ranges::of)
    .collect(Ranges::emptyRangeCountMap, (m, e) -> m.put(e, m.get(e) + 1), Ranges::mergeRangeCountMaps);

rangeCountMap.forEach((k, v) -> System.out.println(k.from() + "\t" + v));

代码的运行结果如下:

    -1      49730
    -0.9    49931
    -0.8    50057
    -0.7    50060
    -0.6    49963
    -0.5    50159
    -0.4    49921
    -0.3    49962
    -0.2    50231
    -0.1    49658
    0       50177
    0.1     49861
    0.2     49947
    0.3     50157
    0.4     50414
    0.5     50006
    0.6     50038
    0.7     49962
    0.8     50071
    0.9     49695

为了类比,我们再生成一百万个高斯伪随机数:

Random random = new Random();
DoubleStream gaussianStream = Stream.generate(random::nextGaussian).mapToDouble(e -> e);
LinkedHashMap<Range, Integer> gaussianRangeCountMap =
    gaussianStream
            .filter(e -> (e >= -1.0 && e < 1.0))
            .limit(1000000)
            .boxed()
            .map(Ranges::of)
            .collect(Ranges::emptyRangeCountMap, (m, e) -> m.put(e, m.get(e) + 1), Ranges::mergeRangeCountMaps);

gaussianRangeCountMap.forEach((k, v) -> System.out.println(k.from() + "\t" + v));
上面代码输出的结果恰恰与我们预期结果相吻合,即: double类型的伪随机数是均匀的分配的,而double类型的高斯伪随机数应该是正态分布的。

附: 完整代码可点击这里获取 https://gist.github.com/bijukunjummen/8129250

译文链接: http://www.importnew.com/9672.html

 java.util.Base64

Java8中java.util.Base64性能比较高,推荐使用。请参考:

  • 性能对比: https://wizardforcel.gitbooks.io/java8-new-features/content/11.html
  • 源代码: http://git.oschina.net/benhail/javase8-sample

该类提供了一套静态方法获取下面三种BASE64编解码器:

1)Basic编码: 是标准的BASE64编码,用于处理常规的需求

// 编码
String asB64 = Base64.getEncoder().encodeToString("some string".getBytes("utf-8"));
System.out.println(asB64); // 输出为: c29tZSBzdHJpbmc=
// 解码
byte[] asBytes = Base64.getDecoder().decode("c29tZSBzdHJpbmc=");
System.out.println(new String(asBytes, "utf-8")); // 输出为: some string

2)URL编码: 使用下划线替换URL里面的反斜线“/”

String urlEncoded = Base64.getUrlEncoder().encodeToString("subjects/demo1?abcd".getBytes("utf-8"));
System.out.println("Using URL Alphabet: " + urlEncoded);
// 输出为:
Using URL Alphabet: c3ViamVjdHMvZGVtbzE_YWJjZA==

3)MIME编码: 使用基本的字母数字产生BASE64输出,而且对MIME格式友好: 每一行输出不超过76个字符,而且每行以“\r\n”符结束。

StringBuilder sb = new StringBuilder();
for (int t = 0; t < 10; ++t) {
  sb.append(UUID.randomUUID().toString());
}
byte[] toEncode = sb.toString().getBytes("utf-8");
String mimeEncoded = Base64.getMimeEncoder().encodeToString(toEncode);
System.out.println(mimeEncoded);

输出为

ODMzZGM3YWEtNGVjNi00N2UxLTg5ZWQtOTY1YTRkZTgyZGJmZDc3OGJlNjMtYzJhNy00MjI3LTkw
YzYtNDc2NGNmYTk1NTU2ZjY0YjllMzgtZDFlOC00M2MwLWJmNDctODcyNmFmM2I4OWRlZGZmMWMy
MjQtYmM4Ny00Yzg4LTlmMzEtNTRmMzBhYzhmYzYxODdhYWQ4YmYtZjFhMi00MzJiLWJhYjMtOTQ5
ZjlkZmNkMmNmNTFlYzQ4NzItYzNkOC00MWIxLThhN2MtODIxZDM0MzA4OGJmODVjNmVmNmQtMjc3
NS00OTllLWFlZGQtNzIwOWM0NmM3OTE4NTZkYWM0MmEtNDJiZS00NDJjLTgyNDQtNWNhMmJkZDVj
NDUyOGYzMjQyZTctNDA1ZC00ZTlkLTlhNzMtM2ZmMDViYmFhNjEyMGRkMzg1ZWMtMzExZS00MGM1
LTlmNjgtZjcwNTQ5ZGEyODAz

StampedLock

它是java8在java.util.concurrent.locks新增的一个API。ReentrantReadWriteLock 在沒有任何读写锁时,才可以取得写入锁,这可用于实现了悲观读取(Pessimistic Reading),即如果执行中进行读取时,经常可能有另一执行要写入的需求,为了保持同步,ReentrantReadWriteLock 的读取锁定就可派上用场。然而,如果读取执行情况很多,写入很少的情况下,使用 ReentrantReadWriteLock 可能会使写入线程遭遇饥饿(Starvation)问题,也就是写入线程迟迟无法竞争到锁定而一直处于等待状态。StampedLock控制锁有三种模式(写,读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁。所谓的乐观读模式,也就是若读的操作很多,写的操作很少的情况下,你可以乐观地认为,写入与读取同时发生几率很少,因此不悲观地使用完全的读取锁定,程序可以查看读取资料之后,是否遭到写入执行的变更,再采取后续的措施(重新读取变更信息,或者抛出异常) ,这一个小小改进,可大幅度提高程序的吞吐量!!

下面是java doc提供的StampedLock一个例子

class Point {
   private double x, y;
   private final StampedLock sl = new StampedLock();
   void move(double deltaX, double deltaY) { // an exclusively locked method
     long stamp = sl.writeLock();
     try {
       x += deltaX;
       y += deltaY;
     } finally {
       sl.unlockWrite(stamp);
     }
   }
  //下面看看乐观读锁案例
   double distanceFromOrigin() { // A read-only method
     long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁
     double currentX = x, currentY = y; //将两个字段读入本地局部变量
     if (!sl.validate(stamp)) { //检查发出乐观读锁后同时是否有其他写锁发生? 
        stamp = sl.readLock(); //如果没有,我们再次获得一个读悲观锁
        try {
          currentX = x; // 将两个字段读入本地局部变量
          currentY = y; // 将两个字段读入本地局部变量
        } finally {
           sl.unlockRead(stamp);
        }
     }
     return Math.sqrt(currentX * currentX + currentY * currentY);
   }
	//下面是悲观读锁案例
   void moveIfAtOrigin(double newX, double newY) { 
      // upgrade
     // Could instead start with optimistic, not read mode
     long stamp = sl.readLock();
     try {
       while (x == 0.0 && y == 0.0) { //循环,检查当前状态是否符合
         long ws = sl.tryConvertToWriteLock(stamp); //将读锁转为写锁
         if (ws != 0L) { //这是确认转为写锁是否成功
           stamp = ws; //如果成功 替换票据
           x = newX; //进行状态改变
           y = newY; //进行状态改变
           break;
         }
         else { //如果不能成功转换为写锁
           sl.unlockRead(stamp); //我们显式释放读锁
           stamp = sl.writeLock(); //显式直接进行写锁 然后再通过循环再试
         }
       }
     } finally {
       sl.unlock(stamp); //释放读锁或写锁
     }
   }
 }

小结:

StampedLock要比ReentrantReadWriteLock更加廉价,也就是消耗比较小。

StampedLock与ReadWriteLock性能对比

是和ReadWritLock相比,在一个线程情况下,是读速度其4倍左右,写是1倍。下图是六个线程情况下,读性能是其几十倍,写性能也是近10倍左右:

 总结

  • synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定;
  • ReentrantLock、ReentrantReadWriteLock,、StampedLock都是对象层面的锁定,要保证锁定一定会被释放,就必须将unLock()放到finally{}中;
  • StampedLock 对吞吐量有巨大的改进,特别是在读线程越来越多的场景下;
  • StampedLock有一个复杂的API,对于加锁操作,很容易误用其他方法;
  • 当只有少量竞争者的时候,synchronized是一个很好的通用的锁实现;
  • 当线程增长能够预估,ReentrantLock是一个很好的通用的锁实现;

StampedLock 可以说是Lock的一个很好的补充,吞吐量以及性能上的提升足以打动很多人了,但并不是说要替代之前Lock的东西,毕竟他还是有些应用场景的,起码API比StampedLock容易入手。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值