Java 8 新特性(二)

346 阅读12分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 2 天,点击查看活动详情

Java 8 新特性(一)

5、Optional

5.1 常用方法

5.1.1 of

为非null的值创建一个Optional。of方法通过工厂方法创建Optional类。需要注意的是,创建对象时传入的参数不能为null。如果传入参数为null,则抛出 NullPointerException 。

5.1.2 ofNullable

为指定的值创建一个Optional,如果指定的值为null,则返回一个空的Optional。

5.1.3 isPresent

如果值存在返回true,否则返回false。

5.1.4 get

如果Optional有值则将其返回,否则抛出NoSuchElementException。

@Test
public void isPresentTest() throws Exception {
   String name = "present";
   Optional<String> optional = Optional.of(name);
   if (optional.isPresent()){
      System.out.println(optional.get());
   }
}

5.1.5 ifPresent

如果Optional实例有值则为其调用consumer,否则不做处理。

5.1.6 orElse

如果有值则将其返回,否则返回指定的其它值。

5.1.7 orElseGet

orElseGet与orElse方法类似,区别在于得到的默认值。orElse方法将传入的字符串作为默认值,orElseGet方法可以接受Supplier接口的实现用来生成默认值。示例如(代码示例 5.1.8 )。

5.1.8 orElseThrow

如果有值则将其返回,否则抛出supplier接口创建的异常。 代码示例 5.1.8 :

@Test
public void orElseTest() throws Exception {
   String v1 = null;
   String v2 = "orElseTest";
   Optional<String> o1 = Optional.ofNullable(v1);
   Optional<String> o2 = Optional.ofNullable(v2);
   //orElse
   System.out.println(o1.orElse("v1 的值为 null"));
   //orElseGet
   System.out.println(o2.orElseGet(() -> "v2 的值为 null"));
   //orElseThrow
   try {
      o1.orElseThrow(() -> new RuntimeException("v1 null "));
   } catch (RuntimeException e) {
      System.out.println(e.getMessage());
   }
}

5.1.9 map

如果有值,则对其执行调用mapping函数得到返回值。如果返回值不为null,则创建包含mapping返回值的Optional作为map方法返回值,否则返回空Optional。 map方法用来对Optional实例的值执行一系列操作。通过一组实现了Function接口的lambda表达式传入操作。

5.1.10 flatMap

如果有值,为其执行mapping函数返回Optional类型返回值,否则返回空Optional。flatMap与map(Funtion)方法类似,区别在于flatMap中的mapper返回值必须是Optional。调用结束时,flatMap不会对结果用Optional封装。 flatMap方法与map方法类似,区别在于mapping函数的返回值不同。map方法的mapping函数返回值可以是任何类型T,而flatMap方法的mapping函数必须是Optional。

@Test
public void mapTest() throws Exception {
   // map
   Optional<String> optional = Optional.ofNullable(null);
   optional = optional.map(v -> v.toUpperCase());
   System.out.println(optional.orElse("null"));

   // flatMap
   Optional<String> o = Optional.of("world");
   o = o.flatMap(v1 -> Optional.of(v1.toUpperCase()));
   System.out.println(o.orElse("null"));
}

5.1.11 filter

如果有值并且满足断言条件返回包含该值的Optional,否则返回空Optional。对于filter函数我们应该传入实现了Predicate接口的lambda表达式,filter个方法通过传入限定条件对Optional实例的值进行过滤。

@Test
public void filterTest() throws Exception {
   Optional<String> optional = Optional.of("hello");
   System.out.println(optional.filter(v -> v.length()> 3).orElse("字符串长度小于等于3"));

   System.out.println(optional.filter(v -> v.length() > 10).orElse("字符串长度小于等于10"));
}

5.2 应用

Optional 解决NPE(java.lang.NullPointerException)问题,它就是为NPE而生的,其中可以包含空值或非空值

@Test
public void test() throws Exception {
   Zoo zoo = new Zoo();
   Optional.ofNullable(zoo).map(o -> o.getDog()).map(dog -> dog.getAge()).ifPresent(
         age -> System.out.println(age)
   );
   Zoo zoo1 = new Zoo();
   Dog dog = new Dog();
   dog.setAge(23);
   zoo1.setDog(dog);
   Optional.ofNullable(zoo1).map(Zoo::getDog).map(Dog::getAge).ifPresent(System.out::println);
}

class Zoo {
   private Dog dog;

   public Dog getDog() {
      return dog;
   }

   public void setDog(Dog dog) {
      this.dog = dog;
   }
}

class Dog {
   private int age;

   public int getAge() {
      return age;
   }

   public void setAge(int age) {
      this.age = age;
   }
}

解决空集合获取元素下标越界异常:

@Test
public void nullTest() throws Exception {
   List<Map<String,String>> list = new ArrayList<Map<String,String>>();
   Map<String,String> m1 = new HashMap<String,String>();
   m1.put("a", "b");
   list.add(m1);
   Optional.ofNullable(list).map(v -> v.isEmpty() ? null : v.get(0)).ifPresent(map -> System.out.println(map));
}

6、Date-Time API

这是对 java.util.Date 强有力的补充,解决了Date类大部分痛点:

  • 非线程安全
  • 时间处理麻烦
  • 各种格式化、时间计算繁琐
  • 设计缺陷,Date类同时包含日期和时间;还有一个 java.sql.Date 容易混淆

6.1 java.time 主要的类

  • LocalDateTime.class // 日期+时间 yyyy-MM-ddTHH:mm:ss.SSS
  • LocalTime.class // 时间 yyyy-MM-dd
  • LocalDate.class // 日期 HH:mm:ss

6.2 格式化

@Test
public void formatTest() throws Exception {
   LocalDate date = LocalDate.now();
   System.out.printf("date format: %s%n", date);

   //.withNano(0) 设置纳秒
   LocalTime time = LocalTime.now().withNano(0);
   System.out.printf("time format: %s%n", time);

   //yyyy-MM-dd HH:mm:ss
   LocalDateTime dateTime = LocalDateTime.now();
   System.out.printf("default dateTime format: %s%n",dateTime);

   String format = dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
   System.out.printf("datetime format: %s%n",format);
}

6.3 字符串转日期格式

Java 8 之前 转换都需要借助 SimpleDateFormat 类,而Java 8 之后只需要 LocalDate、LocalTime、LocalDateTime的 of 或 parse 方法。

@Test
public void strToDateTimeTest() throws Exception {
   LocalDate of = LocalDate.of(2022, 10, 8);
   LocalDate parse = LocalDate.parse("2022-10-01");

   LocalDateTime dateTime = LocalDateTime.of(2022,10,1,11,12,18);
   LocalDateTime parse1 = LocalDateTime.parse("2022-10-09 12:23:22");
}

6.4 日期计算

下面仅以一周后日期为例,其他单位(年、月、日、1/2 日、时等等)大同小异。另外,这些单位都在 java.time.temporal.ChronoUnit 枚举中定义。

@Test
public void computeDateTest() throws Exception {
   //TODO:计算一周后的日期
   LocalDate localDate = LocalDate.now();
   // 方式1
   LocalDate after1 = localDate.plus(1, ChronoUnit.WEEKS);
   // 方式2
   LocalDate after2 = localDate.plusWeeks(1);
   System.out.println("一周后的日期:"+after1);

   //TODO:计算两个日期间隔多少天
oday.with(TemporalAdjusters.firstDayOfMonth());
   System.out.printf("当月第一天:%s%n",firstDayOfMonth);
   // 当月最后一天
   LocalDate lastDayOfMonth = today.with(TemporalAdjusters.lastDayOfMonth());
   System.out.printf("当月最后一天:%s%n",lastDayOfMonth);
   // 当年最后一天
   LocalDate oldDate = LocalDate.parse("1998-09-29");
   // 方式1 间隔几年几月几天
   Period period = Period.between( oldDate,localDate);
   System.out.printf("localDate 与 oldDate 间隔: %s年%s月%s天%n",period.getYears(), period.getMonths(), period.getDays());
   // 方式2 总天数
   long day = localDate.toEpochDay() - oldDate.toEpochDay();
   System.out.println(localDate+"和"+oldDate+" 相差"+day+"天");
}

6.5 获取指定日期

@Test
public void getDate() throws Exception {
   LocalDate today = LocalDate.now();
   // 明天
   LocalDate plusDays = today.plusDays(1);
   System.out.printf("明天:%s%n",plusDays);
   // TemporalAdjusters 时间调整器
   // 当月第一天
   LocalDate firstDayOfMonth = t
   System.out.printf("当年最后一天:%s%n",today.with(TemporalAdjusters.lastDayOfYear()));

   //TODO: 2021年最后一个周日
   LocalDate with = LocalDate.parse("2022-12-10").with(TemporalAdjusters.lastInMonth(DayOfWeek.SUNDAY));
   System.out.printf("2021年最后一个周日:%s%n",with);

}

6.6 JDBC 与 Java 8 时间类型对应关系

现在 jdbc 时间类型和 java8 时间类型对应关系是:

  • Date ---> LocalDate
  • Time ---> LocalTime
  • Timestamp ---> LocalDateTime
  • Java 8之前统统对应 Date,也只有 Date。

6.7 时区

时区:正式的时区划分为每隔经度 15° 划分一个时区,全球共 24 个时区,每个时区相差 1 小时。但为了行政上的方便,常将 1 个国家或 1 个省份划在一起,比如我国幅员宽广,大概横跨 5 个时区,实际上只用东八时区的标准时即北京时间为准。 java.util.Date 对象实质上存的是 1970 年 1 月 1 日 0 点( GMT)至 Date 对象所表示时刻所经过的毫秒数。也就是说不管在哪个时区 new Date,它记录的毫秒数都一样,和时区无关。但在使用上应该把它转换成当地时间,这就涉及到了时间的国际化。java.util.Date 本身并不支持国际化,需要借助 TimeZone。

//北京时间:Wed Jan 27 14:05:29 CST 2021
Date date = new Date();

SimpleDateFormat bjSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//北京时区
bjSdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
System.out.println("毫秒数:" + date.getTime() + ", 北京时间:" + bjSdf.format(date));

//东京时区
SimpleDateFormat tokyoSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
tokyoSdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));  // 设置东京时区
System.out.println("毫秒数:" + date.getTime() + ", 东京时间:" + tokyoSdf.format(date));

//如果直接print会自动转成当前时区的时间
System.out.println(date);
//Wed Jan 27 14:05:29 CST 2021

在新特性中引入了 java.time.ZonedDateTime 来表示带时区的时间。它可以看成是 LocalDateTime + ZoneId。

@Test
public void zonedTest() throws Exception {
   ZonedDateTime zonedDateTime = ZonedDateTime.now();
   
   System.out.printf("当前时区时间:%s%n",zonedDateTime);
   ZoneId zoneId = ZoneId.of(ZoneId.SHORT_IDS.get("JST"));

   ZonedDateTime tokyoTime = zonedDateTime.withZoneSameInstant(zoneId);
   System.out.printf("东京时间:%s%n",tokyoTime);

   LocalDateTime localDateTime = tokyoTime.toLocalDateTime();
   System.out.printf("东京时间转本地时间:%s%n",localDateTime);

   ZonedDateTime localZonedDateTime = localDateTime.atZone(ZoneId.systemDefault());
   System.out.printf("本地时区时间:%s%n",localZonedDateTime);
}

7、类型注解

7.1 什么是类型注解

注解是从Java 5开始引入的新特性,发展到现在已然是遍地开花,在很多框架中得到了广泛的使用,用来简化程序中的配置。在Java 8之前,注解只能在声明的地方使用,比如类、方法、属性;而在Java 8 中,注解可以应用在任何地方,比如

创建实例

new @Interned MyObject();

类型映射

myString = (@NonNull String) str;

接口实现

class UnmodifiableList<T> implements @Readonly List<@Readonly T> { … }

throw exception 声明

void monitorTemperature() throws @Critical TemperatureException { … }

类型注解只是语法而不是语义,并不会影响java的编译时间,加载时间,以及运行时间,也就是说,编译成class文件的时候并不包含类型注解。

7.2 类型注解的作用

类型注解被用来支持在Java的程序中做强类型检查。配合插件式的check framework,可以在编译的时候检测出runtime error,以提高代码质量。这就是类型注解的作用了。 check framework是第三方工具,配合Java的类型注解效果就是1+1>2。它可以嵌入到javac编译器里面,可以配合ant和maven使用, 地址是types.cs.washington.edu/checker-fra…

8、重复注解

允许在同一申明类型(类,属性,或方法)上多次使用同一个注解。 Java 8之前,由另一个注解来存储重复注解,在使用时候,用存储注解Authorities来扩展重复注解。

public class OldRepeatAnnoTest {
   @AuthoritiesOne({@AuthorityOne(role = "Admin"),@AuthorityOne(role = "Manager")})
   public void doSomeThing() {

   }
}
@interface AuthorityOne {
   String role();
}
@interface AuthoritiesOne {
   AuthorityOne[] value();
}

Java 8之后,创建重复注解AuthorityTwo时,加上@Repeatable,指向存储注解AuthoritiesTwo,在使用时候,直接可以重复使用AuthorityTwo注解。这样可读性更强。

public class NewRepeatAnnoTest {

   @AuthorityTwo(role = "admin")
   @AuthorityTwo(role = "message")
   public void doSomeThing() {

   }
}
@Repeatable(AuthoritiesTwo.class)
@interface AuthorityTwo {
   String role();
}

@interface AuthoritiesTwo {
   AuthorityTwo[] value();
}

9、类型推断优化(泛型)

泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。通俗点将就是“类型的变量”。这种类型变量可以用在类、接口和方法的创建中。 泛型的最大优点是提供了程序的类型安全同时可以向后兼容,但也有尴尬的地方,就是每次定义时都要写明泛型的类型,这样显示指定不仅感觉有些冗长,最主要是很多程序员不熟悉泛型,因此很多时候不能够给出正确的类型参数,现在通过编译器自动推断泛型的参数类型,能够减少这样的情况,并提高代码可读性。

9.1 Java 7 优化

在Java 7以前的版本中使用泛型类型,需要在声明并赋值的时候,两侧都加上泛型类型。

// java 7 之前
Map<String, String> map = new HashMap<String, String>();
// java 7 之后
Map<String, String> map = new HashMap<>();

编译器会根据变量声明时的泛型类型自动推断出实例化HashMap时的泛型类型。再次提醒一定要注意new HashMap后面的“<>”,只有加上这个“<>”才表示是自动类型推断,否则就是非泛型类型的HashMap,并且在使用编译器编译源代码时会给出一个警告提示。

9.2 Java 8 优化

Java8里面泛型的目标类型推断主要2个:

支持通过方法上下文推断泛型目标类型 支持在方法调用链路当中,泛型类型推断传递到最后一个方法

class List<E> {
   static <Z> List<Z> nil() { ... };
   static <Z> List<Z> cons(Z head, List<Z> tail) { ... };
   E head() { ... }
}

//通过方法赋值的目标参数来自动推断泛型的类型
List<String> l = List.nil();
//而不是显示的指定类型
//List<String> l = List.<String>nil();
//通过前面方法参数类型推断泛型的类型
List.cons(42, List.nil());
//而不是显示的指定类型
//List.cons(42, List.<Integer>nil());

10、锁优化 StampedLock

10.1 synchronized

在java5之前,实现同步主要是使用synchronized。它是Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。有四种不同的同步块:

  • 实例方法
  • 静态方法
  • 实例方法中的同步块
  • 静态方法中的同步块
public class LockTest {

    private int count;
    
    private static int sum = 0;
    
    public static void main(String[] args) {
    
    }
    /**
     * 实例方法同步
     * Java实例方法同步是同步在拥有该方法的对象上。这样,每个实例其方法同步都同步在不同的对象上,即该方法所属的实例。
     * 只有一个线程能够在实例方法同步块中运行。如果有多个实例存在,那么一个线程一次可以在一个实例同步块中执行操作。一个实例一个线程。
     * @param value
     */
    public synchronized void add(int value) {
        this.count += value;
    }
    
    /**
     * 静态方法同步
     *  静态方法的同步是指同步在该方法所在的类对象上。因为在Java虚拟机中一个类只能对应一个类对象,所以同时只允许一个线程执行同一个类中的静态同步方法。
     * @param value
     */
    public static synchronized void addSum(int value) {
        sum += value;
    }
    
    /**
     * 实例方法中的同步块(与实例方法同步类似)
     * @param value
     */
    public void add1(int value) {
        synchronized (this) {
            this.count += value;
        }
    }
    
    /**
     * 静态方法中的同步块(与静态方法同步类似)
     * @param value
     */
    public static synchronized void addSum1(int value) {
        synchronized (LockTest.class) {
            sum += value;
        }
    }
}

10.2 Lock

它是Java 5在java.util.concurrent.locks新增的一个API。 Lock是一个接口,核心方法是lock(),unlock(),tryLock(),实现类有ReentrantLock, ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock; ReentrantReadWriteLock, ReentrantLock 和synchronized锁都有相同的内存语义。 与synchronized不同的是,Lock完全用Java写成,在java这个层面是无关JVM实现的。Lock提供更灵活的锁机制,很多synchronized 没有提供的许多特性,比如锁投票,定时锁等候和中断锁等候,但因为lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中。

private double x, y; 
private final StampedLock sl = new StampedLock(); 
void m(double deltaX, double deltaY) { 

    long s = sl.writeLock(); 
    try { 
        x += deltaX; 
        y += deltaY; 
    } finally { 
        sl.unlockWrite(s); 
    } 
}

10.3 StampedLock

它是java8在java.util.concurrent.locks新增的一个API。

ReentrantReadWriteLock 在沒有任何读写锁时,才可以取得写入锁,这可用于实现了悲观读取(Pessimistic Reading),即如果执行中进行读取时,经常可能有另一执行要写入的需求,为了保持同步,ReentrantReadWriteLock 的读取锁定就可派上用场。

然而,如果读取执行情况很多,写入很少的情况下,使用 ReentrantReadWriteLock 可能会使写入线程遭遇饥饿(Starvation)问题,也就是写入线程吃吃无法竞争到锁定而一直处于等待状态。

StampedLock控制锁有三种模式(写,读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁。

所谓的乐观读模式,也就是若读的操作很多,写的操作很少的情况下,你可以乐观地认为,写入与读取同时发生几率很少,因此不悲观地使用完全的读取锁定,程序可以查看读取资料之后,是否遭到写入执行的变更,再采取后续的措施(重新读取变更信息,或者抛出异常) ,这一个小小改进,可大幅度提高程序的吞吐量。