Skip to content

右侧栏

JDK8-21

java 8

函数式编程

在Java世界里面,面向对象还是主流思想,对于习惯了面向对象编程的开发者来说,抽象的概念并不陌生。面向对象编程是对数据进行抽象,而函数式编程是对行为进行抽象。这种新的抽象方式还有其他好处。很多人不总是在编写性能优先的代码,对于这些人来说,函数式编程带来的好处尤为明显。程序员能编写出更容易阅读的代码——这种代码更多地表达了业务逻辑,而不是从机制上如何实现。易读的代码也易于维护、更可靠、更不容易出错。在写回调函数和事件处理器时,程序员不必再纠缠于匿名内部类的冗繁和可读性,函数式编程让事件处理系统变得更加简单。能将函数方便地传递也让编写惰性代码变得容易,只有在真正需要的时候,才初始化变量的值。

面向对象编程时对数据进行抽象;函数式编程是对行为进行抽象。

核心思想:使用不可变值和喊出,函数对一个值进行处理,映射成另外一个值

对核心类库的改进主要包括集合类的API和新引入的流Stream。流使程序员可以站在更高的抽象层次上对集合进行操作。

lambda表达式

  • lambda表达式仅能放下如下代码:预定义使用了@Functional注释的函数式接口,自带一个抽象函数的方式,或者SAM(Single Abstract Method 单个方法抽象)类型,这些称为lambda的目标类型,可以用作返回类型,或lambda目标代码的参数,例如,若一个方法接受runnable,comparable或者callable接口,都有单个抽象方法,可以传入lambda表达式。类似的,如果一个方法接受声明于java.util.function包内的接口,例如 Predicate,Function,Consumer或Supplier,那么可以向其传Lambda表达式。

  • Lambda表达式内可以使用 方法引用 仅当方法不修改lambda表达式提供的参数。本例中的lambda表达式可以换位方法引用,因为这仅是一个参数相同的简单方法调用。

    java
    list.forEach(n-> System.out.println(n));
    list.forEach(System.out::println);//使用方法引用
  • 然而,若对参数有任何修改,则不能使用方法引用,而需要键入完整的lambda表达式,如下所示

    java
    list.forEach(x-> System.out.println("s"+x+"s"));
  • lambda内部可以使用静态,非静态和局部变量,这称为lambda内的变量捕获。

  • lambda表达式在java中又称为闭包或匿名函数。

  • lambda方法在编译器内部被翻译成私有方法,并派发invokedynamic字节码指令来进行调用,可以使用JDK中的javap工具来反编译class文件。使用javap -p或javap -c -v 命令看到的lambda表达式生成的字节码大致如下

    java
    private static java.lang.Object lambda$0(java.lang.String);
  • lambda表达式有个限制,那就是只能引用final或final局部变量,这就是说不能在lambda内部修改定义在域外的变量。

    java
    List<Integer> prims =  Arrays.asList(new Integer[]{2,3,6,7});
    int factor  = 2;
    prims.forEach(element -> {facor++;});
    //complie time error:"local variables referenced from a lambda expression must be final or effectively final"
  • 如果在lambda内部访问(使用)而不修改值是可以的,如下所示:

    java
    List<Integer> primes  =  Arrays.asList(new Integer[]{3,4,6,8});
    int factor = 4;
    primes.forEach((element -> {System.out.println(factor * 2)});

分类:惰性求值与及早取值

惰性求值(Lazy Evaluation)是一种编程语言的特性,它在需要运算结果时才会进行计算,而不是在定义时就立即计算。这种方式可以避免不必要的计算,提高程序的效率。

及早求值(Eager evaluation)又译热切求值,也被称为贪婪求值(Greedy evaluation),是多数传统编程语言)的求值策略。在及早求值中,表达式在它被约束到变量的时候就立即求值。这在简单编程语言中作为低层策略是更有效率的,因为不需要建造和管理表示未求值的表达式的中介

java
public class LambdaTest {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("a", "b", "c", "d", "e", "f", "g", "h");
        //惰性求值方法
        Stream<String> a = list.stream().filter(s -> {
            System.out.println(s);//语句并不会打印
            return s.startsWith("a");
        });
        System.out.println("start");
        //只有在调用a.count之后才会打印值
        a.count();
        System.out.println("==================");
        //及早取值方法
        List<String> list2 = list.stream().filter(f -> {
                    System.out.println(f); ////未调用list2 也会打印
                    return f.startsWith("a");
                })
                .collect(Collectors.toList());


    }
}

stream 和 parallelStream

每个stream都有两种模式:顺序执行和并行执行。

顺序流:

java
List <Person> people =  list.getStream.collect(Collectors.toList());

并行流:

java
List<Person> people = list.getStream.parallel().collect(Collectors.toList());

顾名思义,当使用顺序方式去遍历时,每个item读完之后在读下一个item。而并行去遍历时,数组会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。

parallesStream原理:

java
//parallelStream 伪代码实现
List originalList =  someData;
split1 = originalList(0,mid);//将数据分小部分
split2 = originalList(mid,end);
new Runnable(split1.process());//小部分执行操作
new Runnable(split2.process());
List reviseList =  split1 + split2;//将结果合并

大家对hadoop有稍微的了解就知道,里面的MapReduce本省就是用于并行处理大数据集的软件框架,其处理大数据的核心就是大而化小,分配到不同的机器去运行map,最终通过reduce将所有机器的结果结合起来得到一个最终结果,与MapReduce不同,Stream则是利用多核技术可将大数据通过多核并行处理,而MapReduce则可以分布式。

Stream 中的常用方法

  • stream(),parallelStream()
  • filter()
  • findAny(),findFirst()
  • sort()
  • forEach()
  • map(),reduce()
  • flatMap() 将多个Steam连接成一个Stream
  • collect(Collectors.toList())
  • distinct,limit
  • count
  • min,max,summaryStatistics
匿名类简写
java
new Thread(()->{
	System.out.println("lambda 匿名类测试");
})
    //用法
    //(params) -> exporession
    //(params) -> statement
    //(params) -> {statements}
forEach
java
List features = Arrays.asList("a","b","c");
fertures.forEach((n)->{
	System.out.println(n);
})
//使用方法引用
features.forEach(System.out::println);
方法引用

构造引用

java
Supplier<Student> s = new Student();
Supplier<Student> s1 = Student::new;

对象::实例方法,lambda表达式的形参列表与实例方法的(实参列表)类型,个数对应

java
// set.forEach(t -> System.out.println(t));
set.forEach(System.out::println);

类名::静态方法

java
//Stream<Double> stream  = Stream.generate(() -> Math.random());
Stream<Double> stream = Steam.generate(Math::random);

类名::实例方法

java
// TresSet<String> set = new TreeSet((s1,s2)->s1.compareTo(s2));
// 此句编译器提示 Can be replaced with Comparator.natureOrder,这句话告诉我们String已经重写了compareTo()方法,在这里是多次一举,此处可以替换为lambda 方法引用
TreeSet<String> set =  new TresSet(String::compareTo);
Filter & Predicate

常规用法

java
public static void main(String[] args) {
        //filter& predicate
        List<String> list = Arrays.asList("Java", "Scala", "C++", "Haskell", "Lisp");
        System.out.println("start with j");
        filter(list, (s) -> s.startsWith("j"));
        System.out.println("end with j");
        filter(list, (s) -> s.endsWith("l"));
        System.out.println("print all list");
        filter(list, (s) -> true);
        System.out.println("print no list");
        filter(list, (s) -> false);
        System.out.println("print word which length > 4");
        filter(list, (s) -> s.length() > 4);
        //多个predicate组合filter
        Predicate<String> contains = (String s) -> s.startsWith("j");
        Predicate<String> stringFilter = (String s) -> s.length() == 4;
        //可以用and() or() 和xor()逻辑函数来合并predicate
        list.stream().filter(contains.and(stringFilter)).forEach(System.out::println);

    }

    public static void filter(List<String> list, Predicate<String> predicate) {
        list.stream().filter((s) -> predicate.test(s))
                .forEach(System.out::println);
    }
Map&Reduce

map将集合类(例如列表)元素进行转换,还有一个reduce函数可以将所有值合并为一个

java
  //map & reduce
        List<Integer> costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
        double bill = costBeforeTax.stream().map((cost) -> cost + 0.12 * cost).reduce((sum, cost) -> sum + cost).get();
        System.out.println("Total : " + bill);
Collectors
java
//将字符串转换成大小然后收集
        list.stream().map(s -> s.toUpperCase()).collect(Collectors.toList());
  • Collectors.joining("",")
  • Collectors.toList()
  • Collectors.toSet()
  • Collectors.toMap(MemberModel::getUid,Function.identity)// Function.indentity返回的是member对象本身
  • Collectore.toMap(ImageModel::getAid,o->IMAGE_ADDRESS_PREFIX+o.getUrl))
flatMap

将多个Stream连接成一个Stream

java
Stream<String> java = Stream.of("Java", "Scala", "C++", "Haskell", "Lisp");
        Stream<String> scala = Stream.of("Scala", "C++", "Haskell", "Lisp");
        Stream<Stream<String>> StreamOfStream = Stream.of(java, scala);
        Stream<List<String>> list1 = Stream.of(Arrays.asList("Java", "Scala", "C++", "Haskell", "Lisp"), Arrays.asList("Scala", "C++", "Haskell", "Lisp"));
        Stream<String> stringStream = list1.flatMap(strings -> strings.stream());
        Stream<String> stringStream1 = list1.flatMap(strings -> strings.stream());
distinct

去重

java
List<String> collect1 = list.stream().distinct().collect(Collectors.toList());
count

计数

java
//count 计数
long count = list.stream().distinct().count();
match
java
 //anyMatch
 boolean anyMatch = list.stream().anyMatch(s -> s.contains("Java"));
 //allMatch
 boolean allMatch = list.stream().allMatch(s -> s.contains("Scala"));
 //noneMatch
 boolean noneMatch = list.stream().noneMatch(s -> s.contains("Haskell"));
min,max,summaryStatistics
java
//mix最小值
Arrays.asList(1, 3, 6, 7, 2, 9).stream().min(Integer::compareTo).ifPresent(System.out::println);
//max最大值
Arrays.asList(1, 4, 6, 2, 7, 3).stream().max(Integer::compareTo).ifPresent(System.out::println);

如果比较器涉及多个条件可以定制

java
//比较器涉及多个条件场景下,可以定制比较器
        list.stream().max((o1, o2) -> {
            if (o1.length() - o2.length() < 0) {
                //先按照字符长度顺序排序
                return -1;
            }
            //如果字符长度一样,则按照hashcode码顺序排序
            if (o1.hashCode() - o2.hashCode() > 0) {
                return 1;//返回数大于0,会交换o1和o2的位置
            }
            return 0;
        });

summaryStatistics 统计量总结

java
        //summaryStatistics 返回数学统计功能,可以通过返回的对象获取常用数学计算数据
        IntSummaryStatistics intSummaryStatistics = Arrays.asList(1, 54, 1, 754, 85, 5357, 8785, 8578).stream().mapToInt(Integer::valueOf).summaryStatistics();
        System.out.println("最大值" + intSummaryStatistics.getMax());
        System.out.println("最小值" + intSummaryStatistics.getMin());
        System.out.println("求和" + intSummaryStatistics.getSum());
        System.out.println("平均值" + intSummaryStatistics.getAverage());
        System.out.println("个数" + intSummaryStatistics.getCount());
peek

Steam.peek()是一个中间方法,因此在调用终端方法之前它不会执行。

java
Stream.of(10, 20, 30).peek(e -> System.out.println(e)); 
//不会打印任何输出

Stream.of(10,20.30).peek(e -> System.out.println(e)).collect(Collectors.toList())
//因为调用了collect()终端方法,因此会打印数据,同上述的惰性取值与及早取值。

FunctionalInterface

函数式接口

@ FunctionInterface注解

java

/**
 * An informative annotation type used to indicate that an interface
 * type declaration is intended to be a <i>functional interface</i> as
 * defined by the Java Language Specification.
 *
 * Conceptually, a functional interface has exactly one abstract
 * method.  Since {@linkplain java.lang.reflect.Method#isDefault()
 * default methods} have an implementation, they are not abstract.  If
 * an interface declares an abstract method overriding one of the
 * public methods of {@code java.lang.Object}, that also does
 * <em>not</em> count toward the interface's abstract method count
 * since any implementation of the interface will have an
 * implementation from {@code java.lang.Object} or elsewhere.
 *
 * <p>Note that instances of functional interfaces can be created with
 * lambda expressions, method references, or constructor references.
 *
 * <p>If a type is annotated with this annotation type, compilers are
 * required to generate an error message unless:
 *
 * <ul>
 * <li> The type is an interface type and not an annotation type, enum, or class.
 * <li> The annotated type satisfies the requirements of a functional interface.
 * </ul>
 *
 * <p>However, the compiler will treat any interface meeting the
 * definition of a functional interface as a functional interface
 * regardless of whether or not a {@code FunctionalInterface}
 * annotation is present on the interface declaration.
 *
 * @jls 4.3.2 The Class Object
 * @jls 9.8 Functional Interfaces
 * @jls 9.4.3 Interface Method Body
 * @jls 9.6.4.9 @FunctionalInterface
 * @since 1.8
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface {}
  • interface做注解的注解类型,被定义为java语言规范
  • 一个被它注解的接口只能有一个抽象方法,有两种例外
    • 第一个是接口允许有实现的方法,但是方法都必须是default关键字来标记,表示默认实现,java 反射中可以通过java.lang.reflect.Method##isDefault()方法来判断是否是default方法
    • 第二如果生命的方法和java.lang.Object中的某个方法一样,它可以不当做未实现的方法,不违背这个原则:一个被它注解的接口只能有一个抽象方法,比如 java public interface Comparator<T> {int compare(T t1,T t2);boolean equals(Object obj);}
  • 如果一个类型被这个注解修饰,那么编译器会要求这个类必须满足以下条件
    • 这个类必须是一个interface,而不是什么其他的注解,枚举enum或者类class
    • 这个类型必须满足function interface的所有要求。如果包含两个抽象方法则会报错
  • 编译器会自动把满足这些条件的接口自动识别为FunctionInterface

自定义函数接口

java
package com.hongsipeng.jdk8to17.jdk8;

/**
 * @author hongsipeng
 * @apiNote 函数式接口测试类
 * @since 2025/1/14
 */
public class FunctionInterfaceTest {
    public static void main(String[] args) {
        FunctionInterface functionInterface = new FunctionInterface() {
            @Override
            public void test() {
                System.out.println("活到老学到老");
            }
        };

    }
    @FunctionalInterface
    public interface FunctionInterface {
        void test();
    }
}

内置四大函数式接口

  • 消费型接口:Consumer <T> void accept(T t)有参数,无返回值的抽象方法

    比如:map.forEach(BiConsumer(A,T))

    java
    Consumer<String> consumer = (s) -> System.out.println(s);
    consumer.accept("test");
  • 供给型接口:Supplier <T> T get() 无参有返回值的抽象方法;

    Stream().collect(Collector<? super T, A,R> collector)为例;

    比如:

    java
    Supplier<Persion> supplier = Persion::new;
    supplier.get() //new persion

    再如:

    java
    //调用方法
    <R,A> R collect(Collector<? super T,A,R> collector)
    public static <T>
        Collector<T, ?, Set<T>> toSet() {
            return new CollectorImpl<>(HashSet::new, Set::add,
                                       (left, right) -> {
                                           if (left.size() < right.size()) {
                                               right.addAll(left); return right;
                                           } else {
                                               left.addAll(right); return left;
                                           }
                                       },
                                       CH_UNORDERED_ID);
        }
    //CollectorImpl
    private final Supplier<A> supplier,
    private final BiConsumer<A,T> accumulator,
    private final BinaryOperator<A> combiner,
    private final Function<A,R> finisher,
    private final Set<Characteristics> characteristics;
    
    CollectorImpl(Supplier<A> supplier,
                 BiConsumer<A,T> accumulator,
                 BinaryOperator<A> combiner,
                 Function<A,R> finisher,
                 Set<Characteristics> charecteristics){
      this.supplier = supplier;
      this.accumlator = accumulator;
      this.combiner = combiner;
      this.finisher = finisher;
      this.characteristics = characteristics;
    }
    
    CollectorImpl(Supplier<A> supplier,
                 Biconsumer<A,T> accumlator,
                 BinaryOperator<A> combiner,
                 Set<Characteristics characteristics>){
      this(supplier,accumulator,combiner,castingIdentity(),characteristics);
    }
    
    //collect() 方法实现
    public final <R, A> R collect(Collector<? super P_OUT, A, R> collector) {
        A container;
        if (isParallel()
                && (collector.characteristics().contains(Collector.Characteristics.CONCURRENT))
                && (!isOrdered() || collector.characteristics().contains(Collector.Characteristics.UNORDERED))) {
            container = collector.supplier().get();
            BiConsumer<A, ? super P_OUT> accumulator = collector.accumulator();
            forEach(u -> accumulator.accept(container, u));
        }
        else {
            container = evaluate(ReduceOps.makeRef(collector));
        }
        return collector.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)
               ? (R) container
               : collector.finisher().apply(container);
    }
  • 断定型接口:Predicate<T> boolean test(T t),有参数,但是返回值类型是固定的boolean

    比如:stream().filter()中参数就是Predicate

    java
    Predicate<String> predicate = (s) -> s.length() > 0;
    predicate.test("foo") //ture
    predicate.negate().test("foo"); //false
    
    Predicate<Boolean> nonNull = Objects::nonNull;
    Predicate<Boolean> isNull = Objects::isNull;
    Predicate<String> isEmpty = String::isEmpty;
    Pridicate<String> isNotEmpty = isEmpty.nagate();
  • 函数型接口:Function<T,R> R apply (T t),有参数有返回值的抽象方法;

    比如:stream.map() 中参数就是 Function<? super T,? Extend R>;recude()中参数BinaryOperator<T> (ps: BinaryOperator<T> extends Bifunction <T,T,T>)

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

一些例子

  • 输出 年龄>25的程序员中名字排名前3的程序员

    java
    javaProgramers.stream().filter((p) -> p.getAge() > 25)
    	.sort((a,b)->(p.getFirstName().compareTo(p2.getFirstName)))
    	.limit(3)
    	.forEach(p->System.out.printLn(p.getName));
  • 工资最高的java 程序员

    java
    javaProgramers.stream().sort((p1,p2)->{
    	return p2.getSalary() - p1.getSalary();
    }).limit(1);
    
    javaProgrames.stream().max((p1,p2)->(p1.getSalary()-p2.getSalary())).get();
  • 将程序员的姓名存放到treeSet中

    java
    javaProgramers.stream().map(p->p.getName()).collect(toCollection(TreeSet::new));
  • 计算付给程序员的所有工资

    java
    javaProgramers.parallelStream().mapToInt(p->p.getSalary()).sum();
  • 例子总结

    java
    package com.hongsipeng.jdk8to17.jdk8;
    
    import jdk.incubator.vector.VectorOperators;
    
    import java.util.*;
    import java.util.regex.Pattern;
    import java.util.stream.Collectors;
    import java.util.stream.Stream;
    
    /**
     * @author hongsipeng
     * @apiNote 一些方法例子
     * @since 2025/1/18
     */
    public class DemoTest {
        public static void main(String[] args) {
            List<Person> javaProgrammer = Arrays.asList(new Person("jack", 25, 5000L),
                    new Person("jay", 39, 10000L), new Person("tom", 35, 15000L));
            // 输出年龄>25岁的程序员名字排名前3的姓名
            javaProgrammer.stream().filter(person -> person.getAge() > 25)
                    .sorted(Comparator.comparing(Person::getName)).limit(3)
                    .forEach(person -> System.out.println(person.getName()));
            // 工资最高的程序员
            Person person = javaProgrammer.stream().max(Comparator.comparing(Person::getSalary)).get();
            // 将程序员的名称存放到treeSet中
            TreeSet<String> collect = javaProgrammer.stream().map(Person::getName).sorted().collect(Collectors.toCollection(TreeSet::new));
            // 计算所有程序员的工资
            long sum = javaProgrammer.stream().mapToLong(Person::getSalary).sum();
            /**
             * list.sort 和list.stream().sorted()的区别
             * list.sort 会对原始列表修改,效率较高
             * list.stream().sort()返回的是一个新的排序后的流,不会对原始列表进行修改,但是需要更新的内存开销来保存数据
             */
            // 多属性排序,先按照名字排序,再按照薪水排序,如果需要实现反向排序,就在Comparator对象后的reverse()方法
            javaProgrammer.sort(Comparator.comparing(Person::getName, String.CASE_INSENSITIVE_ORDER).thenComparing(Comparator.comparing(Person::getSalary).reversed()));
            // 处理字符串
            // 两个新的方法jdk 8 出现可以在字符串类上使用:join和chars
    
            // join方法使用指令的分隔符,将任意数量的字符串链接为一个字符串
            // String test = "test:java:good";
            String join = String.join(":", "test", "java", "programmer");
            System.out.println(join); // 输出 test:java:programmer
    
    
            // 第二个方法chars从字符串所有字符中创建数据流,所以你可以在这些字符上使用流式操作。
            String charsTest = join.chars().distinct().mapToObj(String::valueOf).sorted().collect(Collectors.joining(","));
            System.out.println(charsTest); //输出
    
            // 不仅仅是字符串,正则表达式模式串也能受益于数据流,我们可以分割任何模式串,并创建数据流来处理它们,而不是将字符串分割为单个字符的数据流,
            Pattern pattern = Pattern.compile(".*@gmail\\.com");
            Stream.of("bob@gmail.com", "alice@hotmail.com")
                    .filter(pattern.asPredicate())
                    .count();
            // 上面的模式串接受任何已@gmail结尾的字符串,并且之后用作java8的predicate来过滤电子邮件地址。
            // => 1
            // 此外,正则模式串可以转换为谓词。这些谓词可以像下面那样用于过滤字符串流
            Pattern.compile(":")
                    .splitAsStream("foobar:foo:bar")
                    .filter(s -> s.contains("bar"))
                    .sorted()
                    .collect(Collectors.joining(":"));
            // => bar:foobar
    
            // 集合-> 取元素的一个属性->去重->组装成list ->返回
            List<String> collect1 = javaProgrammer.stream().map(Person::getName).distinct().collect(Collectors.toList());
            // 集合-> 按照表达式过滤-> 遍历、每个元素处理->放入预先定义的集合中
            List<Object> list = new ArrayList<>();
            javaProgrammer.stream().filter(person1 -> person1.getSalary() > 10000)
                    .forEach(person1 -> {
                        person1.setAge(person1.getAge() + 1);
                        list.add(person1);
                    });
    
            //集合 ->map
            Map<String, Long> collect2 = javaProgrammer.stream().collect(Collectors.toMap(person1 -> {
                return person1.getName();
            }, o -> {
                return o.getSalary();
            }));
            // do模型->model 模型
    
            //phones 是一个List<String>,将相同的元素分组、归类
            List<String> phones=new ArrayList<String>();
            phones.add("a");
            phones.add("b");
            phones.add("a");
            phones.add("a");
            phones.add("c");
            phones.add("b");
            Map<String, List<String>> phoneClassify = phones.stream().collect(Collectors.groupingBy(item -> item));
            System.out.println(phoneClassify);
            // 返回结果
            // {a=[a, a, a], b=[b, b], c=[c]}
    
        }
    }

optional类解析

Java 8 引入了一个新的Option类,Optional类的javadoc 描述是:

这是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。

方法

  • of

    为给null值创建一个Optional。

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

    java
    //调用工厂方法创建Optional
    Optional<String> name = Optional.of("test");
    //传入null参数,则会抛出NullPointerException异常
    Optional<String> someNull = Optional.of(null);
  • ofNullable

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

    ofNullable 方法与of 方法类似,只是of 方法参数如果为null,则会报NullpointException,ofNullable方法允许参数为一个null

    java
    //创建一个不包含任何值的Optional实例,该方法允许参数为null
    Optional empty =  Optional.ofNullable(null);
  • isPresent

    如果值存在则返回true,如果值不存在则返回false

    类似如下代码

    java
    if(optional.isPresent) {
    		//值存在,输出值
      System.out.println(optional.get());
    } else {
      System.out.println(optional.get());//输出null值
    }
  • get

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

    try {
    	//在空的Optional实例上调用get(),抛出NoSuchElementException
    	System.out.println(emptyOptional.get())
    } catch (NoSuchElementException ex){
    	//输出 No value present
    	System.out.println(ex.getMessgae())
    }
  • ifPresent

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

    要理解ifPresent方法,首先需要了解Consumer类。Consumer类包含一个抽象方法,该抽象方法对传入的值进行处理,但没有返回值(有参数无返回值)。java8 后支持使用lambda表达式传入参数。

    如果Optional实例有值,调用ifPresent() 可以接受接口段或lambda表达式。类似下面的代码:

    java
    //ifPresent 方法接受lambda表达式作为参数
    //lambda表达式对Optional的值调用Consumer进行处理
    optional.ifPresent((value)->{
    	System.out.println("optional.ifPresent value is:"+value)
    })
  • orElse

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

    如果Optional实例有值将其返回,否则返回orElse方法传入的参数。示例如下:

    java
    //如果值不为null,orElse方法返回Optional实例的值
    //如果为null,返回传入的消息。
    System.out.println(optional.orElse("There is no value"))
  • orElseGet

    olElseGet 方法与orElse方法类似,区别在与得到的默认值,orElse方法将传入的参数作为默认值,orElseGet方法可以接受Supplier接口的实现来生成默认值,同上述Consumer类似,supplier表示供给型接口,及没有参数有返回值,代码参考如下

    java
    //如果optional值为空,则输出Default Value
    System.out.println(optional.orElseGet(()->{
    	retrun "Default Value"
    }))
  • orElseThrow

    如果有值则将其返回,如果没有值,则抛出Supplier接口创建的异常。

    再orElseGet方法中,我们传入一个Supplier接口。在orElseThrow中我们可以传入一个lambda表达式或方法,如果值不存在则抛出异常。示例如下:

    java
    try{
      //orElseThrow 与orElseGet 类似,但返回的默认值不同,
      //orElseThrow 会抛出lambda表达式或方法生成的异常。
    	optional.orElseThrow(ValueAbsentException::new);
    } catch (ValueAbsentException ex) {
      //输入:No value present in the optional instance
    	System.out.println(ex.getMessage());
    }
  • map

    map 方法文档说明如下:

    如果optional 有值,则对其执行调用 mapping 函数得到返回值。如果返回值不为 null,则创建包含 mapping 返回值的 Optional 作为 map 方法返回值,否则返回空 Optional。

    map方法用来对 Optional 实例的值执行一系列操作,通过一组实现了 Function 接口的 lambda 表达式传入操作。

    java
    //map方法执行传入的 lambda 表达式对 Optianl 实例的值进行修改。
    //为 lambda 表达式的返回值创建新的 Optional 实例作为 map 方法的返回值。
    Optional<String> upperName = name.map((value)->value.toUpperCase());
    System.out.pringtln(upperName.orElse("No value found"))
  • flatMap

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

    flatMap 方法于 Map 方法类似,区别在于mapping 函数的返回值不同,map方法的 mapping 的返回值可以是任何类型的 T,而 flatMap 方法的返回值必须是 Optional。

    java
    //flatMap与map(Function)非常类似,区别在于传入方法的lambda表达式的返回类型。
    //map方法中的lambda表达式返回值可以是任意类型,在map函数返回之前会包装为Optional。 
    //但flatMap方法中的lambda表达式返回值必须是Optionl实例。 
    upperName = name.flatMap((value) -> Optional.of(value.toUpperCase()));
    System.out.println(upperName.orElse("No value found"));//输出SANAULLA
  • filter

    filter 方法通过传入限定条件对 Optional 实例的值进行过滤。

    如果有值并且满足断言条件返回包含该值的 Optional,否则返回 空Optional。

    filter 方法内可以传入实现了 Predicate 接口的 lambda 表达式。

    java
    //filter 方法检查给定的 Optional 值是否满足某些条件
    //如果满足则返回同一个 Optional 实例,否则返回空 Optional
    Optional<String> longName  = name.fiter((value)->value.length()>5);
    System.out.pringln(longName.orElse("The name is less than 5 characters"));
    
    //再举例一个不满足 filter 条件的例子
    Optional<String> anotherName  =  Optional.of("test");
    Optional<String> shortName = anotherName.filter((value)->value.length()>5);
    //输出:name 长度不足 5 个字符
    System.out.println(shortName.orElse("The name is less then 5 characters"))
  • 一个综合的例子

    java
    package com.hongsipeng.jdk8to17.jdk8;
    
    import java.util.NoSuchElementException;
    import java.util.Optional;
    
    /**
     * @author hongsipeng
     * @apiNote optional 测试类
     * @since 2025/3/5
     */
    public class OptionalTest {
        public static void main(String[] args) {
            // 创建 optional实例,也可以通过方法返回值获取
            Optional<String> optional = Optional.of("Test");
            // 创建没有值的 optional 实例,例如值为 NULL
            Optional<String> empty = Optional.ofNullable(null);
            // isPresent 方法用来检测Optional 实例是否有值
            if (optional.isPresent()) {
                // 调用 get 方法或者optional 的值
                System.out.println(optional.get());
            }
    
            // 在 optional实例上调用get(),如果 optional 实例为空 抛出 NoSuchElement Exception
            try {
                empty.get();
            } catch (NoSuchElementException e) {
                throw new RuntimeException(e);
            }
    
            // ifPresent方法接受 lambda 表达式参数。
            /* public void ifPresent(Consumer<? super T> action) {
            if (value != null) {
                action.accept(value);
            }
            }*/
    
            // 如果 optional值不为空,lambda 表达式会处理并在其上执行操作。
            optional.ifPresent((value) -> System.out.println("value = " + value));
    
            // 如果有值orElse方法会返回Optional 实例,否则返回传入的错误信息。
            optional.orElse("optional has no value");
    
            // orElseGet方法于 orElse 方法类似,区别在于 orElseGet 方法接收一个 lambda 值
            optional.orElseGet(() -> {
                return "test"
            });
            // orElseThrow 方法于 orElse方法类似,区别在于返回值。如果oeElseThrow 返回由方法传递的lambda 表达式的返回的异常
            optional.orElseThrow(() -> new RuntimeException("optional has no value"));
    
            // map方法通过传入的 lambda 表达式修改哦 optiona 实例的值,lambada 表达式的返回值会包装为 Optional实例。
            optional.map(String::toUpperCase).orElseThrow(() -> new RuntimeException("optional has no value"));
    
            // flatMap 方法于 map 方法类似,区别在于 lambda 表达式的返回值
            // map方法的 lambda 表达式的返回值是可以是任何类型,但是返回值的接口会被包装为 optional 实例
            // flatMap的lambda 表达式的返回值类型必须是 optional 类型
            optional.flatMap(s -> Optional.of(s.charAt(0))).ifPresent(System.out::println);
    
            // filter方法用于检查 optiona 实例的值是否满足条件,如果满足,则返回 optional 实例,如果不满足返回空 Optional
            optional.filter(s -> !(s.length() > 1)).ifPresent(System.out::println);
            optional.filter(String::isEmpty).orElseThrow(() -> new RuntimeException("optional has no value"));
    
        }
    }
  • 在 java 中提高 Null 的安全性

    假设我们有一个像这样的类的层次结构

    java
    class Outer {
        Nested nested;
        Nested getNested() {
            return nested;
        }
    }
    class Nested {
        Inner inner;
        Inner getInner() {
            return inner;
        }
    }
    class Inner {
        String foo;
        String getFoo() {
            return foo;
        }
    }

    如何解决这种深层次嵌套路径是有些麻烦的,我们必须编写一连串的非 null 检查来防止不会出现 NullpointException;

    java
    Outer outer = new Outer();
    if (outer != null && outer.nested != null && outer.nested.inner != null) {
        System.out.println(outer.nested.inner.foo);
    }

    我们可以通过 java8 的 optional 类来拜托这些校验,map方法接收一个 Function 类型的 lambda 表达式,并自动将每个 function 的结果包装成一个新的 optional 实例。上述的非空判断就可以写成

    java
    Outer outer = new Outer();
    Optional.of(outer).map(Outer::getNested).map(Nester::getInner).map(Inner::getFoo).ifPresent(System.out::pritln)

    还有一种实现相同作用的方式就是通过利用一个 supplier 函数来解决嵌套路径问题

    java
    
    public static <T> Optional<T> resolve(Supplier<T> resolver) {
        try {
            T result = resolver.get();
            return Optional.ofNullable(result);
        }
        catch (NullPointerException e) {
            return Optional.empty();
        }
    }
    
    Outer obj = new Outer();
    resolve(() -> obj.getNested().getInner().getFoo())
        .ifPresent(System.out::println);

    调用 obj.getNested().getInner().getFoo()) 可能会抛出一个 NullPointerException 异常。在这种情况下,该异常将会被捕获,而该方法会返回 Optional.empty()。

    这两个解决方法可能没有传统的 null 检查那么高的性能,不过在大多是情况下不会有太多问题。

默认方法

默认方法就是接口有实现方法,而且不需要实现类去实现其方法,只需要在方法前加个 default 关键字即可。

为什么要有这个特性?因为接口设计是个双刃剑,好吃是面向抽象而不是面向具体编程,缺陷是,当需要修改接口时候,需要修改全部实现该接口的类,目前的 java8 之前的集合框架没有 forEach 方法,通常能够想到的解决办法是在 JDK 里给相关的接口给添加新的方法及实现。然而,对于,已经发布的版本是没法在接口中添加新的方法同时不影响已有的实现,所以引进默认方法。他们的目的是为了解决接口的修改与现有的实现不兼容的问题。

java 8 抽象类和接口对比

相同点不同点
都是抽象类型抽象类不可以多重继承,接口可以
都可以有实现方法(java8之前没有默认方法的接口没有实现方法)抽象类和接口所反映出的设计理念不同。其实抽象类表示的是 is-a 关系,接口表示的是 like-a 关系
都可以不需要实现类或者继承者去实现所有方法接口中定义的变量默认是 public static final 型,且必须给其初值,所以实现类中不能改变其值;抽象类中的变量默认是 friendly 型,其值可以在子类中重新定义,也可以重新赋值。

多重继承的冲突

由于一个类可能继承多个接口,同一个方法可以从个不同的接口引入,自然而然的会有冲突的现象,默认方法判断冲突的规则如下:

  1. 一个声明在类里面的方法优先于任何默认方法(classes always win)
  2. 否则,则会优先取路径最短的。

例子

  • case 1

    java
     		public interface A {
            default void print() {
                System.out.println("A");
            }
        }
        public interface B {
            default void print() {
                System.out.println("B");
            }
        }
        public static class C implements A,B{
    		//报错 com.hongsipeng.jdk8to17.jdk8.DefaultMethodTest.C inherits unrelated defaults for print() from types com.hongsipeng.jdk8to17.jdk8.DefaultMethodTest.A and com.hongsipeng.jdk8to17.jdk8.DefaultMethodTest.B
        }
      
    }

    如何一定要这么写,同时实现 A、B 两个接口,并且同时使用其中一个接口的默认方法,可以用到语法 X.super.m(...):

    java
    public static class C implements A,B{
    
            @Override
            public void print() {
              //通过重写实现方法时需指定使用哪一个接口的默认方法
                B.super.print();
            }
        }
  • Case 2

    java
    package com.hongsipeng.jdk8to17.jdk8;
    
    import com.sun.tools.javac.Main;
    
    /**
     * @author hongsipeng
     * @apiNote java 8 默认方法测试类
     * @since 2025/3/17
     */
    public class DefaultMethodTest2 {
        public interface A {
            default void method() {
                System.out.println("A.method");
            }
        }
        public interface B {
            default void method() {
                System.out.println("B.method");
            }
        }
        public interface C extends A,B {
           default void method() {
               System.out.println("C.method");
           }
        }
        public static class D implements A,B,C {
    
        }
    
        public static void main(String[] args) {
            D a = new D();
            a.method(); //输出 C.method 因为 C是最短路径
        }
    }
  • case 3

    java
    package com.hongsipeng.jdk8to17.jdk8.defaultmethod;
    
    /**
     * @author hongsipeng
     * @apiNote java 8 默认方法测试 3
     * @since 2025/3/17
     */
    public class DefaultMethodTest3 {
        public interface A {
            default void method() {
                System.out.println("A.method");
            }
        }
        public interface B extends A {
            default void method() {
                System.out.println("B.method");
            }
        }
        public static class C implements B {
    
        }
    
        public static void main(String[] args) {
            C c = new C();
            c.method(); //输出 B.method
        }
    }

    通过上述例子对比可知,当有多个同名的默认方法时,默认寻找的是最短路径的方法,如果存在有多个最短路径,则报错。

类型注解

什么是类型注解

注解大家都知道,从 java5 开始加入了这个特性,现在基本所有的框架中都在广泛使用注解,用来简化配置或者实现代理等功能。那么什么时候类型注解呢?

  1. 在 java 8 之前,注解只能是在声明的地方所使用,比如类,方法,属性;

  2. 在 java 8 里面,注解可以应用在任何地方,比如:

    创建类实例

    java
    new @Interned MyObject();

    类型映射

    java
    myString = (@NonNull String) str;

    Implements 语句中

    java
    class UnmodifiableList<T> implements @Readonly List<@Readonly T> {...}

    Throw exception声明

    java
    void monitorTemperature throws @Critical TemperatureException {...}

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

类型注解的作用

先看看下面的代码,

java
Collections.emptyList().add("One");
int i = Integer.parseInt("hello");
System.console().readLine();

上面的代码编译是通过的,但运行是会分别报

UnsupportedOperationException;

NumberFormatException;

NullPointerException异常,这些都是 runtime error;

类型注解被用来支持在 Java 的程序中做强类型检查。配合插件式的 check framework,可以在编译的时候检测出 runtimeError,以提高代码质量。这就是类型注解的作用了。

Check frameword 是第三方工具,配合 Java 的类型注解效果就是 1+1>2。它可以嵌入到 javac编译器里面,可以配合 ant 和 maven 使用,地址是 http://types.cs.washingtoon.edu/checker-frameword/。check framework 可以找到类型注解出现的地方并检查,举个简单的例子:

java
import checkers.nullness.quals.*;
public class GetStarted {
    void sample() {
        @NonNull Object ref = new Object();
    }
}

使用 java 编译上面的类

shell
javac -processor checkers.nullness.NullnessChecker GetStarted.java

编译是通过,但如果修改成

java
@NonNull Object ref = null;

再次编译,则出现

java
GetStarted.java:5: incompatible types.
found   : @Nullable <nulltype>
required: @NonNull Object
        @NonNull Object ref = null;
                              ^
1 error

类型注解向下兼容的解决方案

如果你不想使用类型注解检测出来错误,则不需要processor,直接javac GetStarted.java是可以编译通过的,这是在java 8 with Type Annotation Support版本里面可以,但java 5,6,7版本都不行,因为javac编译器不知道@NonNull是什么东西,但check framework 有个向下兼容的解决方案,就是将类型注解nonnull用/**/注释起来,比如上面例子修改为

java
import checkers.nullness.quals.*;
public class GetStarted {
    void sample() {
        /*@NonNull*/ Object ref = null;
    }
}

这样javac编译器就会忽略掉注释块,但用check framework里面的javac编译器同样能够检测出nonnull错误。 通过类型注解+check framework我们可以看到,现在runtime error可以在编译时候就能找到。

关于 JSR308

JSR 308想要解决在Java 1.5注解中出现的两个问题:

  • 在句法上对注解的限制: 只能把注解写在声明的地方
  • 类型系统在语义上的限制: 类型系统还做不到预防所有的bug

JSR 308 通过如下方法解决上述两个问题:

  • 对Java语言的句法进行扩充,允许注解出现在更多的位置上。包括: 方法接收器(method receivers,译注: 例public int size() @Readonly { … }),泛型参数,数组,类型转换,类型测试,对象创建,类型参数绑定,类继承和throws子句。其实就是类型注解,现在是java 8的一个特性
  • 通过引入可插拔的类型系统(pluggable type systems)能够创建功能更强大的注解处理器。类型检查器对带有类型限定注解的源码进行分析,一旦发现不匹配等错误之处就会产生警告信息。其实就是check framework

对JSR308,有人反对,觉得更复杂更静态了,比如

java
@NotEmpty List<@NonNull String> strings = new ArrayList<@NonNull String>()>

换成动态语言为

java
var strings = ["one", "two"];

有人赞成,说到底,代码才是“最根本”的文档。代码中包含的注解清楚表明了代码编写者的意图。当没有及时更新或者有遗漏的时候,恰恰是注解中包含的意图信息,最容易在其他文档中被丢失。而且将运行时的错误转到编译阶段,不但可以加速开发进程,还可以节省测试时检查bug的时间。

重复注解

什么是重复注解

允许在同一申明类型(类,属性,或方法)的多次使用同一个注解

JDK8 之前

java 8之前也有重复使用注解的方案,但可读性不是很好,比如下面的代码

java
public @interface Authority {
     String role();
}

public @interface Authorities {
    Authority[] value();
}

public class RepeatAnnotationUseOldVersion {

    @Authorities({@Authority(role="Admin"),@Authority(role="Manager")})
    public void doSomeThing(){
    }
}

由另外一个注解来存储重复注解,在使用的时候,用存储注解 Authorities 来扩展重复注解。

JDK8 重复注解

我们再来看看 java8中的做法

java
@Repeatable(Authorities.class)
public @interface Authority {
     String role();
}

public @interface Authorities {
    Authority[] value();
}

public class RepeatAnnotationUseNewVersion {
    @Authority(role="Admin")
    @Authority(role="Manager")
    public void doSomeThing(){ }
}

不同的地方是,创建重复注解 Authority 时,加上@Repeatabel,指向存储注解 Authorities,在使用时候,直接可以重复使用 Authority 注解。从上面例子看出,java8 里面更适合常规的思维,可读性更强。

类型推断优化

简单理解泛型

泛型是 java 1.5 中的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。通俗点说就是“类型的变量”。这种类型的变量可以用在类,接口和方法的创建中。

理解 java 泛型最简单的方法就是把它看成一个便捷语法,能节省你某些 java 类型转换的操作

java
List<Apple> box = new ArrayList<Apple>();
box.add(new Apple());
Apple apple = box.get(0);

上面的代码自身表达的很清楚,box 是一个装有 Apple 对象的 List。get 方法返回一个 Apple对象实例,这个过程不需要进行类型转换。没有泛型,上面的代码需要 get()后再强转类型

java
Apple apple = (Apple) box.get(0);

泛型的尴尬

泛型最大的优点是提供了程序的类型安全同时又可以向后兼容,但也有尴尬的地方,就是每次定义时都要写明泛型的类型,这样显示指定类型不仅感觉有些冗长,最主要时很多程序员不熟悉泛型,因此很多时候不能够正确的给出类型参数,现在通过编译器自动推断泛型的参数类型,能够减少该情况的发生,并提高代码的可读性。

java7 的泛型类型推断改进

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

java
Map<String,String> myMap = new HashMap<String,String>();

你可能觉得:老子在声明变量的的时候已经指明了参数类型,为毛还要在初始化对象时再指定? 幸好,在Java SE 7中,这种方式得以改进,现在你可以使用如下语句进行声明并赋值:

java
Map<String, String> myMap = new HashMap<>(); //注意后面的"<>"

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

但是:java se7 在创建泛型实例时的类型推断是有限制的:只有构造器的参数化类型在上下问中被显著的声明了,才可以使用类型推断,否则不行。例如:下面的例子在 java7 无法正确编译(但现在 java8 里面可以编译,因为根据方法参数来自动推断泛型的类型);

java
List<String> list = new ArrayList<>();
list.add("A");//由于 addAll 期望获得 Collection<? extends String>类型的参数,因此下面的语句无法通过
list.addAll(new ArrayList<>());

java8 的泛型类型推断改进

java8里面泛型的目标类型推断只要 2 个:

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

让我们看看官网的例子

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

根据 JEP101 的特性,我们在调用上面方法的时候可以这样写

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

JRE精简

Oracle公司如期发布了Java 8正式版!没有让广大javaer失望。对于一个人来说,18岁是人生的转折点,从稚嫩走向成熟,法律意味着你是完全民事行为能力人,不再收益于未成年人保护法,到今年为止,java也走过了18年,java8是一个新的里程碑,带来了前所未有的诸多特性,lambda表达式,Stream API,新的Date time api,多核并发支持,重大安全问题改进等,相信java会越来越好,丰富的类库以及庞大的开源生态环境是其他语言所不具备的,说起丰富的类库,很多同学就吐槽了,java该减肥了,确实是该减肥,java8有个很好的特性,即JEP161(http://openjdk.java.net/jeps/161 ),该特性定义了Java SE平台规范的一些子集,使java应用程序不需要整个JRE平台即可部署和运行在小型设备上。开发人员可以基于目标硬件的可用资源选择一个合适的JRE运行环境。

JRE 精简的好处

  • 更小的 Java 环境需要更少的计算资源
  • 一个较小的运行时环境可以更好的优化性能和启动时间。
  • 消除未使用的代码从安全的角度总是好的。
  • 这些打包的应用程序可以下载速度更快。

移除 Permgen

很多开发者都在其系统中见过“java.lang.OutOfMemoryError:PermGen space” 这一个问题。这往往是由类加载器相关的内存泄露以及新类加载器的创建导致的,通常出现于代码热部署时,相对于正式产品,该问题在开发机上出现的频率更高,在产品中最常见的“问题”是默认值太低了。常用的解决方法是将其设置为 256MB 或更高。

PermGen Space 介绍

PermGen Space 的全称是 Permanent Generation space,是指内存的永久保存区域,说说为什么会内存溢出:这一部分用于存放 Class 和 Meta 信息,Class 在被 load 的时候被放入 PermGen Space 区域,它和存放 Instance 的 Heap 区域不同,所以,如果你的 App 会 load 很多 Class 的话,就很可能会出现 PermGen space 错误。这种错误常见在 web 服务器 对JSP 进行 pre complie 的时候。

JVM 种类有很多,比如 Oralce-Sun Hotspot,Oralce JRockit,IBM J9,Taobao jvm 等。需要注意的是,PermGen Space 是Oralce-Sun Hotspot 才有。

元空间(MetaSpace)一种新的内存空间诞生

JDK8 Hostspot JVM 将移除永久区,使用本地内存来存储类元数据信息并称之为:元空间(Metaspace);这与Oracle JRockit 和IBM JVM’s很相似,image-20250325004905533

这意味着不会再有java.lang.OutOfMemoryError: PermGen问题,也不再需要你进行调优及监控内存空间的使用……但请等等,这么说还为时过早。在默认情况下,这些改变是透明的,接下来我们的展示将使你知道仍然要关注类元数据内存的占用。请一定要牢记,这个新特性也不能神奇地消除类和类加载器导致的内存泄漏。

java 8中 metaspace 总结如下:

  • PermGen 空间的状况

    这部分内存空间将全部移除。

    JVM 的参数:PermSize 和 MaxPermSize 会被忽略并给出警告(如果在启用时设置了这两个参数)。

  • Metaspace 内存分配模型

    大部分元数据都在本地内存中分配。

    用于描述类元数据的“klasses”已经被移除。

  • Metaspace 容量

    默认情况下,类元数据只受可用的本地内存限制(容量取决于是32位或是64位操作系统的可用虚拟内存大小)。

    新参数(MaxMetaspaceSize)用于限制本地内存分配给类元数据的大小。如果没有指定这个参数,元空间会在运行时根据需要动态调整。

  • Metaspace垃圾回收

    对于僵死的类及类加载器的垃圾回收将在元数据使用达到“MaxMetaspaceSize”参数的设定值时进行。

    适时地监控和调整元空间对于减小垃圾回收频率和减少延时是很有必要的。持续的元空间垃圾回收说明,可能存在类、类加载器导致的内存泄漏或是大小设置不合适。

  • Java 堆内存的影响

    一些杂项数据已经移到Java堆空间中。升级到JDK8之后,会发现Java堆 空间有所增长。

  • Metaspace 监控

    元空间的使用情况可以从HotSpot1.8的详细GC日志输出中得到。

    Jstat 和 JVisualVM两个工具,在使用b75版本进行测试时,已经更新了,但是还是能看到老的PermGen空间的出现。

    前面已经从理论上充分说明,下面让我们通过“泄漏”程序进行新内存空间的观察……

PermGen vs. Metaspace 运行时比较

为了更好地理解 Metaspace 内存空间的运行时行为,将进行以下几种场景的测试:

  1. 使用 JDK1.7 运行 java 程序,监控并耗尽默认设定的85MB 大小的 PermGen内存空间。
  2. 使用 JDK1.8 运行 Java 程序,监控新 Metaspace 内存空间的动态增长和垃圾回收过程。
  3. 使用 JDK1.8 运行程序,模拟耗尽通过'MaxMetaspaceSize' 参数设定的 128MB 大小的 Metaspace 内存空间。

首先建立一个模拟 PermGen OOM 的代码

java
public class ClassA {
	public void method(String name) {
		//do nothing
	}
}

上面是一个简单的 ClassA,把他编译成 class 字节码放到 D:/classes下面,测试代码中用URLClassLoader来加载此类编译后的 class 文件,

java
/**
 * 模拟PermGen OOM
 */
public class OOMTest {
    public static void main(String[] args) {
        try {
            //准备url
            URL url = new File("D:/classes").toURI().toURL();
            URL[] urls = {url};
            //获取有关类型加载的JMX接口
            ClassLoadingMXBean loadingBean = ManagementFactory.getClassLoadingMXBean();
            //用于缓存类加载器
            List<ClassLoader> classLoaders = new ArrayList<ClassLoader>();
            while (true) {
                //加载类型并缓存类加载器实例
                ClassLoader classLoader = new URLClassLoader(urls);
                classLoaders.add(classLoader);
                classLoader.loadClass("ClassA");
                //显示数量信息(共加载过的类型数目,当前还有效的类型数目,已经被卸载的类型数目)
                System.out.println("total: " + loadingBean.getTotalLoadedClassCount());
                System.out.println("active: " + loadingBean.getLoadedClassCount());
                System.out.println("unloaded: " + loadingBean.getUnloadedClassCount());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

虚拟机器参数设置如下: -verbose:gc 设置-verbose参数是为了获取类型加载和卸载的信息

JDK1.8 @64-bit-PermGen 耗尽测试

Java1.7的 PerGen 默认空间为 85MB(或者可以通过-XX:MaxPermSize=XXXm指定)image-20250327010834536

可以从上面的 JVisualVM 的截图看出:当加载超过 6万个类之后,PermGen被耗尽。我们也能通过程序和GC的输出观察耗尽的过程。

程序输出(摘取了部分)

......
[Loaded ClassA from file:/D:/classes/]
total: 64887
active: 64887
unloaded: 0
[GC 245041K->213978K(536768K), 0.0597188 secs]
[Full GC 213978K->211425K(644992K), 0.6456638 secs]
[GC 211425K->211425K(656448K), 0.0086696 secs]
[Full GC 211425K->211411K(731008K), 0.6924754 secs]
[GC 211411K->211411K(726528K), 0.0088992 secs]
...............
java.lang.OutOfMemoryError: PermGen space

JDK 1.8 @64-bit -Metaspace大小动态调整测试

Java 的 Metaspace空间:不受限制(默认)image-20250327011218887

从上面的截图可以看到,JVM Metaspace 进行了动态拓展,本地内存的使用由 20MB 增长到 646MB,以满足程序中不断增长的类数据内存占用需求。我们也能观察到 JVM 的垃圾回收事件—试图销毁僵死的类或类加载器对象。但是,由于我们程序的泄漏,JVM别无选择只能动态扩展Metaspace内存空间。程序加载超过10万个类,而没有出现OOM事件。

JDK 1.8 @64-bit – Metaspace 受限测试

Java的Metaspace空间:128MB(-XX:MaxMetaspaceSize=128m)image-20250327011542752

可以从上面的JVisualVM的截图看出:当加载超过2万个类之后,Metaspace被耗尽;与JDK1.7运行时非常相似。我们也能通过程序和GC的输出观察耗尽的过程。另一个有趣的现象是,保留的原生内存占用量是设定的最大大小两倍之多。这可能表明,如果可能的话,可微调元空间容量大小策略,来避免本地内存的浪费。

从 Java 程序的输出中看到如下异常。

[Loaded ClassA from file:/D:/classes/]
total: 21393
active: 21393
unloaded: 0
[GC (Metadata GC Threshold) 64306K->57010K(111616K), 0.0145502 secs]
[Full GC (Metadata GC Threshold) 57010K->56810K(122368K), 0.1068084 secs]
java.lang.OutOfMemoryError: Metaspace

在设置了MaxMetaspaceSize的情况下,该空间的内存仍然会耗尽,进而引发“java.lang.OutOfMemoryError: Metadata space”错误。因为类加载器的泄漏仍然存在,而通常Java又不希望无限制地消耗本机内存,因此设置一个类似于MaxPermSize的限制看起来也是合理的。

总结

  1. 之前不管是不是需要,JVM都会吃掉那块空间……如果设置得太小,JVM会死掉;如果设置得太大,这块内存就被JVM浪费了。理论上说,现在你完全可以不关注这个,因为JVM会在运行时自动调校为“合适的大小”;
  2. 提高Full GC的性能,在Full GC期间,Metadata到Metadata pointers之间不需要扫描了,别小看这几纳秒时间;
  3. 隐患就是如果程序存在内存泄露,像OOMTest那样,不停的扩展metaspace的空间,会导致机器的内存不足,所以还是要有必要的调试和监控。

StampedLock

synchronized

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

有四种不同的同步块

  1. 实例方法
  2. 静态方法
  3. 实例方法中的同步块
  4. 静态方法中的同步块

大家对此应该不陌生,所以不多讲了,以下是代码示例

java
synchronize(this){
	// do operation
}

小结:在多线程并发编程中 Synchronized 一直是元老级角色,很多人都会称呼它为重量级锁,但是随着Java SE1.6对Synchronized进行了各种优化之后,性能上也有所提升。

Lock

java
rwlock.writeLock().lock();
try{
	//do operation
} finally {
	rwlock.writeLock().unlock();
}

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

Lock 是一个接口,核心方法是 lock(),unlock(),tryLock(),实现类有 ReentrantLock,ReentrntReadWriteLock.ReadLock,ReentrantReadWriteLock.WriteLock;

ReentrantReadWriteLock,ReentrantLock 和 synchronized 锁都有相同的内存语义。与 synchronized 不同的是,Lock 完全用 Java 写成,在 java 这个层面是无关 JVM 实现的。Lock 提供的更灵活的锁机制,很多 synchronized 没有提供的许多特性,比如锁投票,定时锁等候和中断锁等候,但因为 Lock 是通过代码实现的,要保证锁一定会被释放,就必须将 unLock()放到 finally() 中。下面是 lock 代码实例

java
class Point {
  private double x, y;
  private final StamperLock 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 optimistics, 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);//释放读锁与写锁
    }
  }
}

小结:比 synchronized 更加灵活、更具可伸缩性的锁定机制,但不管怎么说还是synchronized代码要更容易书写些

StampedLock

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

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

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

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

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

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

java
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倍。image-20250329021129594

下图是六个线程情况下,读性能是其几十倍,写性能也是近10倍左右:image-20250329021152614

下图是吞吐量提高:image-20250329021226398

总结

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

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

LocalDate/LocalDateTime

java8 之前的 Date 有那些槽点

Tiago Fernandez 做过一次投票,选举最烂的 JAVA API,排第一的是 EJB2.X,第二的就是日期 API。

槽点一

最开始的时候,Date 既要承载日期信息,又要做日期之间的转换,还要做不同日期格式的显示,职责较复杂(不符合单一职责的设计模式要求)后来从 JDK1.1 开始,这三项职责分开了,原有的 Data 中的相应方法已经废弃,不过,无论是 Date,还是 Calendar,都用这不太方便,这是 API 没有设计好的地方。

槽点二

坑爹的 year 和 month

java
Date date = new Date(2012,1,1);
System.out.println(date);
//输出 Thu Feb 01 00:00:00 CST 3912

观察输出结果,year 是 2012+1900,而 month参数给的是 1, 结果输出的是二月份。

应该曾有人告诉你,如果你要设置日期,应该使用java.util.Calendar,像这样

java
Calendar calendar =  Calendar.getInstance();
calendar.set(2013,8,2);

这样写又不对了,calendar 的 month 也是从0 开始计数的,表达8 月份应该用 7 这个数字,要没就干脆用枚举

java
calendar.set(2013,Calendar.AUGUST,2);

注意上面的代码,Calendar 年份的传值不需要减去 1900(当然月份的定义和 Date 还是一样)。

槽点 三

java.util.Date与 java.util.Calendar 中的所有属性都是可变的下面的代码,计算两个日期之间的天数....

java
public static void main(String [] args){
	Calendar birth = Calendar.getInstance();
  birth.set(1975,Calendar.MAY,26);
  Calendar now = Calendar.getInstance();
  System.out.println(daysBetween(birth,now));
  System.out.println(daysBetween(birth,now));//第二次的结果和第一次的结果不一致,为 0
}
public static long daysBetween(Calendar begin,Calendar end) {
  long daysBetween = 0;
  while(begin.before(end)){
    begin.add(Calendar.DAY_OF_MONTH,1);
    daysBetween++;
  }
  return daysBetween;
}

daysBetween 有点问题,如果连续计算两个 Date 实例的话,第二次会取得 0,因为 Calendar 状态是可变的,考虑到重复计算场合,最好复制一个新的 Calendar

java
public static long daysBetween(Calendar begin,Calendar end) {
  Calendar calendar = (Calendar) begin.clone();//复制
  long daysBetween = 0;
  while(calendar.before(end)){
    calendar.add(Clendar.DAY_OF_MONTH,1);
    daysBetween++;
	}
  return daysBetween;
}
槽点 4

SimpleDateTimeFormat 是非线程安全的。

Java8 时间和日期

类概览

Java8 仍然延用了 ISO 的日历体系,并且与它的前辈们不同,java.time包中的类是不可变且线程安全的。新的时间及日期 API 位于 java.time包中,下面是里面的一些关键的类:

  • Instant——它代表的是时间戳
  • LocalDate——不包含具体时间的日期,比如 2014-01-14。它可以用来存储生日,周年纪念日,入职日期等。
  • LocalTime——它代表的是不含日期的时间
  • LocalDateTime——它包含了日期及时间,不过还是没有偏移信息或者说时区。
  • ZonedDateTime——这是一个包含时区的完整的日期时间,偏移量是以 UTC/格林威治时间为基准的。

新的库还增加了ZoneOffset 及 Zoned,可以为时区提供更好的支持。有了新的 DateTimeFormatter 之后日期的解析及格式化也变得焕然一新了。

方法概览

该包的 API 提供了大量相关的方法,这些方法一般有一致的方法前缀:

  • of:静态工厂方法
  • parse:静态工厂方法,关注与解析
  • get:获取某些东西的值
  • is:检查某些东西的是否是 true
  • with:不可变的 setter 等价物
  • plus:加一些量到某个对象
  • minus:从某个对象减去一些量
  • to:转换到另一个类型
  • at:把这个对象与另一个对象组合起来,例如:date.atTime(time)
一些例子
java

java 9

java 10

java 11

java 12

java 13

java 14

java 15

java 16

java 17

接口私有方法(JDK9)

本地变量类型推断(JDK10)

HTTP Client(JDK11)

Switch 表达式(JDK14)

文本块(JDK15)

instanceof 的模式匹配(JDK16)

Record (JDK 16)

Sealed Class (JDK17)

函数式编程

函数式编程基础、函数式编程API

curry,composite,transducer

functor,monad

lambda演算及运算子

JDK 新特性详解

Java模块化

ZGC垃圾回收

诊断和监控

字符串压缩

java flow API

新一代JIT 编译器 Graal

本站访客数 人次 本站总访问量