Java 8 核心新特性

前置认知 :Java8是Java里程碑版本,核心革新是引入函数式编程,核心作用是将业务逻辑作为参数传递,彻底简化匿名内部类、集合遍历、空值处理等冗余代码,大幅提升代码简洁度与开发效率。

一、Lambda 表达式

Lambda表达式是Java8核心语法糖,核心作用是简化匿名内部类写法,实现行为参数化,把业务逻辑像变量一样传递给方法,大幅精简代码。

Java8之前,传递简单业务逻辑必须编写冗长的匿名内部类,有效代码占比极低。而Lambda可直接精简核心逻辑,代码更直观。核心使用前提(新手必记) :Lambda表达式仅能配合函数式接口使用,非函数式接口无法使用Lambda。

1.1 核心语法结构

通用格式:(参数列表) -> { 方法逻辑代码 }

语法拆解(零基础秒懂):

  • (参数列表):和普通方法的参数列表一致,无参数写空括号,单个参数可省略括号

  • ->:Lambda专属符号,固定写法,相当于"指向、执行"的意思

  • {}:方法体,存放具体逻辑,单行代码可省略大括号和return

简化规则(日常开发90%场景都用简化写法):

  1. 参数只有1个:可省略参数括号

  2. 方法体只有1行代码:可省略大括号,有返回值时自动省略return

  3. 无参数、多行代码:必须保留括号和大括号

1.2 函数式接口(前置概念)

函数式接口定义:有且仅有一个抽象方法的接口。Lambda表达式本质就是对这个唯一抽象方法的快速实现。

Java8提供 @FunctionalInterface 注解,用于编译期校验接口是否符合函数式接口规范。该注解可省略,只要接口仅有一个抽象方法,就是函数式接口,只是缺少编译校验。

1.3 完整可运行代码示例

java 复制代码
import java.util.ArrayList;
import java.util.List;

/**
 * Lambda表达式零基础演示
 * 包含普通写法、简化写法、集合遍历实战
 */
public class LambdaDemo {
    public static void main(String[] args) {
        // 1. 无参数Lambda演示(Runnable是典型函数式接口)
        // 旧写法:匿名内部类,代码冗余
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("旧写法:线程执行任务");
            }
        }).start();

        // Lambda简化写法(无参数,单行逻辑)
        new Thread(() -> System.out.println("Lambda写法:线程执行任务")).start();

        // 2. 单参数Lambda演示(集合遍历forEach)
        List<String> nameList = new ArrayList<>();
        nameList.add("张三");
        nameList.add("李四");
        nameList.add("王五");

        // 旧写法:增强for循环
        for (String name : nameList) {
            System.out.println("普通循环:" + name);
        }

        // Lambda简化遍历(单参数省略括号,单行省略大括号)
        nameList.forEach(name -> System.out.println("Lambda遍历:" + name));

        // 3. 多参数、多行逻辑Lambda演示
        // 自定义计算逻辑,两个参数,多行代码
        calculate(10, 20, (a, b) -> {
            int sum = a + b;
            System.out.println("两数之和:" + sum);
            return sum;
        });
    }

    // 自定义方法,参数为函数式接口,支持Lambda传参
    public static void calculate(int x, int y, Calc calc) {
        calc.calc(x, y);
    }
}

// 自定义函数式接口
@FunctionalInterface
interface Calc {
    int calc(int a, int b);
}

1.4 本节高频面试题(含标准答案)

题1:简述Lambda表达式的核心作用与使用前提

答:核心作用是简化匿名内部类代码,实现行为参数化,让代码更简洁优雅。使用前提是必须依托函数式接口,接口有且仅有一个抽象方法,Lambda用于对该抽象方法做逻辑实现。

题2:Lambda表达式的语法简化规则有哪些?

答:单参数可省略参数括号;单行方法体可省略大括号,有返回值自动省略return;无参数、多参数、多行逻辑需保留对应括号和大括号,无特殊简化。

题3:Lambda表达式能否访问外部局部变量?有什么限制?

答:可以访问外部局部变量,但变量必须是隐式final(不可二次赋值)。Java8无需手动加final,只要变量未被二次修改,编译器自动判定为常量,修改会直接编译报错。

二、函数式接口

上章节讲解了函数式接口的基础概念,本章重点讲解Java8内置的四大核心函数式接口。这四个接口是官方封装的通用接口,无需开发者自定义,是Lambda、Stream流的核心基础,适配绝大多数业务逻辑场景。

所有内置函数式接口均位于 java.util.function 包下,无需死记硬背,重点区分入参、返回值、适用场景即可,其余衍生接口原理完全一致。

2.1 Predicate 断言型接口

核心特点:入参1个,返回值boolean(true/false)

核心用途:做条件判断、数据筛选,接收参数后返回真假结果,是Stream过滤操作的底层依赖接口。

抽象方法boolean test(T t)

java 复制代码
import java.util.function.Predicate;

/**
 * Predicate断言接口演示:条件判断、数据筛选
 */
public class PredicateDemo {
    public static void main(String[] args) {
        // 场景:判断数字是否大于10
        Predicate<Integer> predicate = num -> num > 10;
        // 执行判断
        System.out.println(predicate.test(5));  // false
        System.out.println(predicate.test(15)); // true

        // 进阶:多条件拼接(且、或、非)
        // 判断:大于5 且 小于20
        Predicate<Integer> p1 = num -> num > 5;
        Predicate<Integer> p2 = num -> num < 20;
        System.out.println(p1.and(p2).test(10)); // true
    }
}

2.2 Consumer 消费型接口

核心特点:入参1个,无返回值

核心用途:消费处理数据,接收参数后执行业务逻辑(遍历、打印、修改),无返回值,常用于集合遍历、数据批量处理。

抽象方法void accept(T t)

java 复制代码
import java.util.function.Consumer;

/**
 * Consumer消费接口演示:处理数据、无返回值
 */
public class ConsumerDemo {
    public static void main(String[] args) {
        // 场景:接收姓名,格式化打印
        Consumer<String> consumer = name -> System.out.println("用户姓名:" + name);
        consumer.accept("张三");
        consumer.accept("李四");

        // 进阶:andThen 连续处理数据
        Consumer<String> c1 = s -> System.out.print("原始姓名:" + s);
        Consumer<String> c2 = s -> System.out.println(",姓名长度:" + s.length());
        // 先执行c1,再执行c2
        c1.andThen(c2).accept("王五");
    }
}

2.3 Function 函数型接口

核心特点:入参1个,返回值1个(入参和返回值类型可不同)

核心用途:数据转换、类型映射,接收参数后加工处理,返回全新数据,入参和返回值类型可不同,是Stream数据转换的核心接口。

抽象方法R apply(T t)

java 复制代码
import java.util.function.Function;

/**
 * Function函数接口演示:数据转换、映射
 */
public class FunctionDemo {
    public static void main(String[] args) {
        // 场景:字符串转整数
        Function<String, Integer> function = str -> Integer.parseInt(str);
        Integer num = function.apply("123");
        System.out.println("转换后的数字:" + num);

        // 进阶:链式转换 字符串转数字,再计算平方
        Function<String, Integer> f1 = s -> Integer.parseInt(s);
        Function<Integer, Integer> f2 = n -> n * n;
        // 先执行f1,再执行f2
        Integer result = f1.andThen(f2).apply("10");
        System.out.println("链式转换结果:" + result); // 100
    }
}

2.4 Supplier 供给型接口

核心特点:无入参,有返回值

核心用途:生成数据,无需传入参数,直接返回数据,常用于默认值生成、随机数据创建、对象实例化。

抽象方法T get()

java 复制代码
import java.util.Random;
import java.util.function.Supplier;

/**
 * Supplier供给接口演示:生成数据、无参有返回
 */
public class SupplierDemo {
    public static void main(String[] args) {
        // 场景:生成随机100以内整数
        Supplier<Integer> supplier = () -> new Random().nextInt(100);
        System.out.println("随机数:" + supplier.get());
        System.out.println("随机数:" + supplier.get());

        // 场景:获取默认字符串
        Supplier<String> strSupplier = () -> "默认用户";
        System.out.println(strSupplier.get());
    }
}

2.5 本节高频面试题(含标准答案)

题1:简述Java8四大核心函数式接口的区别与适用场景

答:Predicate(断言):有参无返,返回布尔,用于条件筛选;Consumer(消费):有参无返,用于数据处理;Function(函数):有参有返,用于数据转换;Supplier(供给):无参有返,用于数据生成。

题2:@FunctionalInterface注解的作用是什么?可以省略吗?

答:作用是编译期校验接口是否只有一个抽象方法,避免定义错误。可以省略,只要接口满足函数式接口规范,依然可配合Lambda使用,只是失去编译校验功能。

题3:函数式接口中可以有默认方法和静态方法吗?

答:可以。函数式接口仅限制抽象方法只能有一个,默认方法、静态方法属于非抽象方法,数量不限,不影响函数式接口的判定。

三、Stream API

Stream API是Java8优化集合操作的核心工具,专门用于简化List、Set、Map的遍历、筛选、转换、聚合、分组等操作,替代传统繁琐的for循环+嵌套判断写法。

核心区分:集合用于存储数据,Stream用于处理数据。Stream不存储任何数据,仅对集合数据做流水线处理,操作完成后自动销毁。

Stream核心特性:惰性求值。中间操作仅记录处理逻辑,不会立即执行,只有触发终端操作后,所有逻辑才会一次性执行,减少集合遍历次数,提升处理性能。

Stream操作分为两类(新手必记):

  • 中间操作:filter、map、sorted等,返回新Stream,可链式拼接,惰性执行

  • 终端操作:collect、forEach、reduce、count等,触发执行,结束流水线

3.1 核心操作详解+完整代码示例

包含五大高频操作:filter过滤、map转换、collect收集、reduce聚合、完整流式流程

java 复制代码
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Stream API 零基础完整实战
 * 包含filter、map、collect、reduce核心操作
 */
public class StreamDemo {
    public static void main(String[] args) {
        // 初始化测试集合
        List<Integer> numList = new ArrayList<>();
        numList.add(1);
        numList.add(2);
        numList.add(3);
        numList.add(4);
        numList.add(5);
        numList.add(6);

        // 1. filter 中间操作:过滤数据(保留符合条件的数据)
        // 场景:保留所有偶数
        List<Integer> evenList = numList.stream()
                .filter(num -> num % 2 == 0) // 筛选条件:能被2整除
                .collect(Collectors.toList()); // 终端操作:流转集合
        System.out.println("过滤后的偶数:" + evenList);

        // 2. map 中间操作:数据转换、映射
        // 场景:所有数字乘以2
        List<Integer> doubleList = numList.stream()
                .map(num -> num * 2) // 数据加工:每个数*2
                .collect(Collectors.toList());
        System.out.println("翻倍后的数字:" + doubleList);

        // 3. reduce 终端操作:聚合计算(求和、求最值、累积运算)
        // 场景:集合所有数字求和
        Integer sum = numList.stream()
                .reduce(0, Integer::sum); // 初始值0,累加所有元素
        System.out.println("集合总和:" + sum);

        // 4. 完整流式链式操作(开发最常用)
        // 需求:筛选偶数 -> 数字翻倍 -> 收集为新集合
        List<Integer> resultList = numList.stream()
                .filter(num -> num % 2 == 0) // 过滤偶数
                .map(num -> num * 2) // 数据翻倍
                .sorted() // 自然排序
                .collect(Collectors.toList()); // 收集结果
        System.out.println("链式处理最终结果:" + resultList);

        // 5. 统计操作:计数
        long count = numList.stream().count();
        System.out.println("集合元素总数:" + count);
    }
}

3.2 进阶核心操作(分组、分区、多条件处理)+ 通俗总结

上文讲解了Stream基础的过滤、转换、聚合操作,实际开发中,数据分组、分区归类是高频刚需(订单分组、用户分层、数据统计)。本节重点讲解单条件分组、多条件多级分组、布尔分区三种核心进阶用法,覆盖主流业务数据归类场景。

3.2.1 单条件分组(groupingBy 基础用法)

根据单个字段或单个自定义条件对数据归类,相同条件的元素归为一组,最终返回 Map<分组键, 元素集合>,是日常开发最常用的分组方式。

java 复制代码
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * Stream 单条件分组演示
 * 场景:根据学生性别分组
 */
public class StreamGroupSingleDemo {
    public static void main(String[] args) {
        // 初始化学生测试数据
        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student("张三", "男", 18));
        studentList.add(new Student("李四", "女", 17));
        studentList.add(new Student("王五", "男", 19));
        studentList.add(new Student("赵六", "女", 18));

        // 单条件分组:根据性别分组
        Map<String, List<Student>> genderGroup = studentList.stream()
                .collect(Collectors.groupingBy(Student::getGender));

        // 遍历分组结果
        genderGroup.forEach((gender, students) -> {
            System.out.println("性别【" + gender + "】分组数据:" + students);
        });
    }
}

// 学生实体类
class Student {
    private String name;
    private String gender;
    private Integer age;

    public Student(String name, String gender, Integer age) {
        this.name = name;
        this.gender = gender;
        this.age = age;
    }

    // getter方法
    public String getGender() { return gender; }
    public String getName() { return name; }
    public Integer getAge() { return age; }

    @Override
    public String toString() {
        return "Student{name='" + name + "', age=" + age + "}";
    }
}

3.2.2 多条件分组(多级分组)

针对复杂业务场景,支持两层及以上条件嵌套分组。先通过第一级条件粗分组,再在每组内通过第二级条件细分组,返回嵌套Map结构,适配多维数据统计。

java 复制代码
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * Stream 多条件多级分组演示
 * 场景:先按性别分组,再按是否成年(18岁)二次分组
 */
public class StreamGroupMultiDemo {
    public static void main(String[] args) {
        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student("张三", "男", 18));
        studentList.add(new Student("李四", "女", 17));
        studentList.add(new Student("王五", "男", 19));
        studentList.add(new Student("赵六", "女", 18));
        studentList.add(new Student("孙七", "男", 16));

        // 多条件分组:先性别、再成年状态
        Map<String, Map<Boolean, List<Student>>> multiGroup = studentList.stream()
                .collect(Collectors.groupingBy(
                        // 第一级分组条件:性别
                        Student::getGender,
                        // 第二级分组条件:年龄是否≥18
                        Collectors.groupingBy(student -> student.getAge() >= 18)
                ));

        // 遍历多级分组结果
        multiGroup.forEach((gender, ageGroup) -> {
            System.out.println("一级分组:性别【" + gender + "】");
            ageGroup.forEach((isAdult, students) -> {
                String status = isAdult ? "成年" : "未成年";
                System.out.println("  二级分组:" + status + ":" + students);
            });
        });
    }
}

3.2.3 分区操作(partitioningBy)

partitioningBy是特殊的分组方式,仅支持布尔类型条件,强制将数据划分为 true、false 两组。相较于groupingBy,二分类场景下性能更优,适用于合格/不合格、达标/不达标等场景。

java 复制代码
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * Stream 分区操作演示
 * 场景:区分成年、未成年学生
 */
public class StreamPartitionDemo {
    public static void main(String[] args) {
        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student("张三", "男", 18));
        studentList.add(new Student("李四", "女", 17));
        studentList.add(new Student("王五", "男", 19));

        // 分区:分为≥18岁、<18岁两组
        Map<Boolean, List<Student>> partitionMap = studentList.stream()
                .collect(Collectors.partitioningBy(student -> student.getAge() >= 18));

        System.out.println("成年学生:" + partitionMap.get(true));
        System.out.println("未成年学生:" + partitionMap.get(false));
    }
}

3.2.4 核心操作通俗总结

  • filter:筛选过滤,保留符合条件的元素,依赖Predicate接口

  • map:元素转换、数据映射,依赖Function接口

  • collect:终端收集,将Stream流转为List、Set、Map等集合,支持分组、分区、统计

  • reduce:聚合汇总,将集合元素合并为单个结果(求和、最值、累积运算)

  • groupingBy单分组:单条件数据归类,是日常开发最常用的分组方式

  • groupingBy多分组:多级嵌套分组,适配复杂多维数据统计场景

  • partitioningBy分区:布尔二分类专用,性能优于普通groupingBy分组

新手重点踩坑 :Stream流为一次性资源,执行终端操作后自动关闭、销毁,重复调用中间或终端操作会抛出 IllegalStateException 异常。

3.3 本节高频面试题(含标准答案)

题1:Stream的中间操作和终端操作有什么区别?

答:中间操作是惰性执行,仅记录逻辑不运行,可链式拼接;终端操作触发所有中间操作执行,结束流生命周期。无终端操作时,中间操作代码永远不会执行。

题2:简述Stream的惰性求值原理及优势

答:惰性求值即延迟执行,中间操作只封装逻辑,终端操作才统一执行。优势是减少循环次数,避免无效计算,链式操作一次性遍历集合,大幅提升数据处理性能。

题3:Stream和普通for循环相比,优缺点是什么?

答:优点:代码简洁、可读性强、支持链式编程、天然支持并行流;缺点:基础循环场景性能略低于for循环,复杂逻辑调试难度稍高,新手易出现流复用报错。

题4:Stream流可以重复使用吗?为什么?

答:不可以。Stream是一次性资源,触发终端操作后流会自动关闭、销毁,再次调用中间或终端操作会抛出IllegalStateException异常。

四、接口默认方法与静态方法

Java8之前,接口仅支持抽象方法,一旦接口新增方法,所有实现类必须强制重写,否则编译报错,严重影响项目迭代扩展。Java8新增默认方法、静态方法,核心目的是解决接口版本升级兼容问题,接口新增方法无需改动原有实现类。

说白了,Java8新增接口默认方法和静态方法,核心目的就是解决接口升级兼容问题(接口新增方法,旧实现类无需修改,直接兼容),完美适配项目迭代、框架版本更新场景。

4.1 默认方法(default)

被default关键字修饰的方法,拥有方法体,属于接口实例方法。实现类可直接继承使用,也可按需重写,无需强制实现。

易错点:若一个类同时实现多个接口,且多个接口存在同名默认方法,必须手动重写该方法,否则编译器无法识别调用逻辑,直接编译报错。

java 复制代码
/**
 * 接口默认方法演示
 * 解决接口新增方法的兼容问题
 */
public interface DefaultMethodInterface {
    // 传统抽象方法(必须被实现类重写)
    void sayHello();

    // Java8新增默认方法(有方法体,实现类可直接使用)
    default void showMsg() {
        System.out.println("这是接口的默认方法,无需强制重写");
    }
}

// 接口实现类
class DefaultDemoImpl implements DefaultMethodInterface {
    // 必须重写抽象方法
    @Override
    public void sayHello() {
        System.out.println("实现类重写抽象方法");
    }

    // 可选:重写默认方法,不重写则使用接口默认逻辑
    /*
    @Override
    public void showMsg() {
        System.out.println("实现类重写接口默认方法");
    }
    */
}

class TestDefault {
    public static void main(String[] args) {
        DefaultDemoImpl impl = new DefaultDemoImpl();
        impl.sayHello();
        // 直接调用接口默认方法
        impl.showMsg();
    }
}

4.2 接口静态方法(static)

Java8允许接口定义static静态方法,静态方法属于接口本身,和实现类无关。仅能通过「接口名.方法名」调用,不被实现类继承、不可被重写。多用于封装接口专属工具方法。

适用场景:通用工具逻辑封装,无需单独创建工具类,简化代码结构。

java 复制代码
/**
 * 接口静态方法演示
 */
public interface StaticMethodInterface {
    // 接口静态方法
    static void staticToolMethod() {
        System.out.println("这是接口的静态工具方法,只能通过接口名调用");
    }
}

class TestStatic {
    public static void main(String[] args) {
        // 正确调用:接口名.静态方法
        StaticMethodInterface.staticToolMethod();

        // 错误调用:实现类、对象均无法调用静态方法
        // StaticDemoImpl.staticToolMethod();
    }
}

4.3 本节高频面试题(含标准答案)

题1:Java8为什么要引入接口默认方法?

答:为了解决接口迭代兼容问题。旧版本接口新增抽象方法,所有实现类必须重写,代码改动量大;默认方法可保留默认逻辑,旧实现类无需修改,完美兼容新版本接口。

题2:接口默认方法和静态方法的核心区别是什么?

答:默认方法通过实现类对象调用,可被继承、重写;静态方法通过接口名调用,不被继承、不可重写。默认方法面向实例,静态方法面向接口工具逻辑。

题3:类实现多个接口,存在同名默认方法会报错吗?怎么解决?

答:会编译报错,因为编译器无法判定调用哪个接口的默认逻辑。解决方案是手动重写该同名默认方法,自定义实现逻辑。

五、Optional 类

Optional是Java8专为解决**空指针异常(NPE)**设计的工具类,替代传统繁琐的 if (obj != null) 空值判断,从语法层面规范空值处理逻辑,提升代码健壮性。

Optional本质是对象容器,内部封装目标对象,强制开发者主动处理空值场景,避免直接调用null对象方法导致程序崩溃。

核心设计思想:将被动空指针报错,转变为主动空值处理,标准化空值逻辑。

5.1 常用核心方法+完整代码演示

涵盖新手必备方法:ofNullable、isPresent、orElse、orElseGet、orElseThrow

java 复制代码
import java.util.Optional;

/**
 * Optional空值处理工具类零基础实战
 * 彻底解决空指针异常
 */
public class OptionalDemo {
    public static void main(String[] args) {
        // 1. 创建Optional对象
        // ofNullable:支持null和非null数据(日常开发99%用这个)
        String str1 = null;
        Optional<String> optionalNull = Optional.ofNullable(str1);

        String str2 = "Java8学习";
        Optional<String> optionalStr = Optional.ofNullable(str2);

        // of:仅支持非null数据,传null直接报错(不推荐新手使用)
        // Optional<String> errorOpt = Optional.of(null);

        // 2. isPresent():判断是否有非空值(true=非空,false=空)
        if (optionalNull.isPresent()) {
            System.out.println("数据非空:" + optionalNull.get());
        } else {
            System.out.println("当前数据为空");
        }

        // 3. orElse():为空时返回默认值,非空返回原数据
        String result1 = optionalNull.orElse("默认空值数据");
        System.out.println("orElse结果:" + result1);

        // 4. orElseGet():为空时执行Lambda生成默认值(性能优于orElse)
        String result2 = optionalNull.orElseGet(() -> "Lambda生成默认数据");
        System.out.println("orElseGet结果:" + result2);

        // 5. orElseThrow():为空时抛出异常(参数校验常用)
        try {
            String result3 = optionalNull.orElseThrow(() -> new RuntimeException("数据不能为空!"));
        } catch (RuntimeException e) {
            System.out.println("异常提示:" + e.getMessage());
        }

        // 非空数据获取
        System.out.println("非空数据:" + optionalStr.orElse("空值"));
    }
}

5.2 新手必懂方法区别

  • orElse:无论数据是否为空,默认值都会提前初始化,适合简单常量默认值场景

  • orElseGet:仅当数据为空时,才通过Lambda生成默认值,复杂场景性能更优

高危****踩坑:禁止直接使用get()方法取值!若Optional封装null,调用get()会直接抛出空指针异常,必须先判空或使用orElse等兜底方法。

5.3 本节高频面试题(含标准答案)

题1:Optional类的设计初衷是什么?解决了什么问题?

答:初衷是优雅处理空值,杜绝空指针异常。替代传统繁琐的if空判断,将空值处理标准化,强制开发者处理空场景,提升代码健壮性和可读性。

题2:of()和ofNullable()的区别是什么?日常开发用哪个?

答:of()传入null会直接空指针报错,仅支持非空数据;ofNullable()支持null和非空数据。日常开发统一使用ofNullable,安全无风险。

题3:orElse和orElseGet的核心区别?

答:orElse无论空与否都会创建默认值;orElseGet仅空值时才生成默认值。复杂默认场景优先用orElseGet,性能更优,简单常量场景可通用orElse。

六、新日期时间 API

Java8之前的Date、Calendar API存在诸多缺陷:线程不安全、月份从0开始计数、API繁琐、时间对象可修改、格式化工具并发报错,日常开发极易产生bug。

Java8全新推出java.time 包日期API,核心优势:不可变、线程安全、语法直观、API简洁。所有时间修改操作均返回新对象,原对象不发生改变,彻底解决旧API并发问题。

核心常用类:LocalDate(纯日期)、LocalTime(纯时间)、LocalDateTime(日期+时间,最常用)、Period(日期间隔)、Duration(时间间隔)、DateTimeFormatter(线程安全格式化)。

6.1 核心类完整代码演示

java 复制代码
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Period;
import java.time.format.DateTimeFormatter;

/**
 * Java8新日期时间API零基础全演示
 * 包含日期、时间、间隔、格式化、时间计算
 */
public class DateTimeDemo {
    public static void main(String[] args) {
        // 1. LocalDate:获取当前日期(年-月-日)
        LocalDate nowDate = LocalDate.now();
        System.out.println("当前日期:" + nowDate);
        // 自定义日期
        LocalDate customDate = LocalDate.of(2026, 8, 1);
        System.out.println("自定义日期:" + customDate);

        // 2. LocalTime:获取当前时间(时:分:秒)
        LocalTime nowTime = LocalTime.now();
        System.out.println("当前时间:" + nowTime);

        // 3. LocalDateTime:日期+时间(最常用)
        LocalDateTime nowDateTime = LocalDateTime.now();
        System.out.println("当前日期时间:" + nowDateTime);

        // 4. 日期时间格式化(线程安全,替代SimpleDateFormat)
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String formatTime = nowDateTime.format(formatter);
        System.out.println("格式化后时间:" + formatTime);

        // 字符串转日期时间
        LocalDateTime parseTime = LocalDateTime.parse("2026-08-01 14:00:00", formatter);
        System.out.println("字符串解析时间:" + parseTime);

        // 5. Period:计算年月日间隔(仅日期)
        Period period = Period.between(customDate, nowDate);
        System.out.println("日期间隔:" + period.getYears() + "年" + period.getMonths() + "月" + period.getDays() + "日");

        // 6. Duration:计算时分秒间隔(仅时间)
        LocalTime time1 = LocalTime.of(10, 0, 0);
        LocalTime time2 = LocalTime.of(12, 30, 0);
        Duration duration = Duration.between(time1, time2);
        System.out.println("时间间隔:" + duration.toHours() + "小时" + duration.toMinutesPart() + "分钟");

        // 7. 日期时间加减计算
        LocalDateTime nextDay = nowDateTime.plusDays(1); // 加1天
        LocalDateTime lastHour = nowDateTime.minusHours(1); // 减1小时
        System.out.println("明天此时:" + nextDay);
        System.out.println("一小时前:" + lastHour);
    }
}

6.2 核心类通俗区分 + 高频API详解 + 新旧日期转换

6.2.1 五大核心类通俗区分

  • LocalDate:仅存储年月日,适用于生日、下单日期、归档日期等无时间场景

  • LocalTime:仅存储时分秒,适用于打卡时间、时段统计等无日期场景

  • LocalDateTime:存储年月日+时分秒,绝大多数业务表单、日志、订单场景首选

  • Period:仅用于LocalDate,计算年月日级别的日期间隔

  • Duration:用于LocalTime/LocalDateTime,计算时分秒毫秒级时间间隔

6.2.2 开发高频API详解(增删改查/判断/格式化)

以下为开发高频复用的日期API,包含时间加减、前后对比、字段截取、合法性校验、格式化解析,覆盖所有日常时间操作场景。

java 复制代码
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Period;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;

/**
 * Java8新日期 高频API全套实战
 * 包含加减、对比、判断、截取、格式化通用操作
 */
public class DateTimeApiFullDemo {
    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();
        System.out.println("当前时间:" + now);

        // 1. 时间加减API(精准单位控制)
        LocalDateTime plus3Day = now.plusDays(3);       // 加3天
        LocalDateTime minus2Hour = now.minusHours(2);  // 减2小时
        LocalDateTime plus1Year = now.plus(1, ChronoUnit.YEARS); // 通用单位加1年
        System.out.println("3天后时间:" + plus3Day);
        System.out.println("2小时前时间:" + minus2Hour);

        // 2. 时间对比API(前后、相等判断)
        LocalDateTime targetTime = LocalDateTime.of(2026, 8, 1, 12, 0);
        boolean isBefore = now.isBefore(targetTime); // 当前时间是否早于目标时间
        boolean isAfter = now.isAfter(targetTime);   // 当前时间是否晚于目标时间
        boolean isEqual = now.isEqual(targetTime);   // 时间是否相等
        System.out.println("是否早于2026-08-01:" + isBefore);

        // 3. 时间截取、单独获取年月日时分秒
        int year = now.getYear();
        int month = now.getMonthValue(); // 直接返回1-12,无需+1
        int day = now.getDayOfMonth();
        int hour = now.getHour();
        System.out.printf("当前:%d年%d月%d日 %d时%n", year, month, day, hour);

        // 4. 日期合法性自动校验(新API自带校验,杜绝非法日期)
        // 非法日期会直接抛出异常,无需手动判断
        // LocalDate errorDate = LocalDate.of(2026, 2, 30); // 2月30日非法,编译运行报错

        // 5. 格式化与解析(全局通用格式)
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String formatTime = now.format(formatter);
        LocalDateTime parseTime = LocalDateTime.parse(formatTime, formatter);
        System.out.println("格式化时间:" + formatTime);
    }
}

6.2.3 传统Date/Calendar 与 新日期API 互转(重点刚需)

老旧项目改造、第三方接口对接时,经常需要新旧日期类型互转。以下为生产环境通用的无坑互转代码,完美兼容旧项目Date、Calendar类型。

java 复制代码
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Calendar;
import java.util.Date;

/**
 * 新旧日期API互转工具(生产可用)
 * Date、Calendar 与 LocalDateTime/LocalDate 双向转换
 */
public class DateTimeConvertDemo {
    public static void main(String[] args) {
        // 1. 旧Date 转 新LocalDateTime、LocalDate
        Date oldDate = new Date();
        LocalDateTime dateToDateTime = oldDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
        LocalDate dateToDate = oldDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        System.out.println("Date转LocalDateTime:" + dateToDateTime);
        System.out.println("Date转LocalDate:" + dateToDate);

        // 2. 新LocalDateTime 转 旧Date
        LocalDateTime nowDateTime = LocalDateTime.now();
        Date dateFromDateTime = Date.from(nowDateTime.atZone(ZoneId.systemDefault()).toInstant());
        System.out.println("LocalDateTime转Date:" + dateFromDateTime);

        // 3. 旧Calendar 转 新日期API
        Calendar calendar = Calendar.getInstance();
        LocalDateTime calendarToDateTime = calendar.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
        System.out.println("Calendar转LocalDateTime:" + calendarToDateTime);

        // 4. 新日期 转 Calendar
        Calendar newCalendar = Calendar.getInstance();
        newCalendar.setTime(Date.from(nowDateTime.atZone(ZoneId.systemDefault()).toInstant()));
    }
}

6.2.4 新旧日期API核心痛点对比总结

旧API(Date/Calendar)核心痛点

  • 线程不安全:SimpleDateFormat多线程并发会数据错乱

  • 设计反人类:月份0-11,获取真实月份需要+1,极易出错

  • 对象可变:set方法修改时间,会改变原对象数据,存在并发风险

  • API繁琐:日期加减、对比、间隔计算需要大量冗余代码

新API(java.time)核心优势

  • 天然线程安全:不可变对象,修改返回新对象,无并发问题

  • 符合日常认知:月份1-12,日期时间规则直观

  • API极简:一行代码实现加减、对比、间隔计算

  • 自带合法性校验、线程安全格式化工具

核心总结:新日期对象为不可变类,修改操作会生成新对象,不修改原数据,天然规避多线程并发数据错乱问题。

6.3 本节高频面试题(含标准答案)

题1:Java8新日期API相比旧Date/Calendar有什么优势?

答:新API线程安全、对象不可变,无并发报错问题;月份从1开始,符合日常认知;API简洁易懂,格式化工具线程安全,时间计算、间隔统计更便捷,彻底规避旧版本各类bug。

题2:Period和Duration的区别是什么?

答:Period用于日期间隔 (年月日),基于LocalDate计算;Duration用于时间间隔(时分秒毫秒),基于LocalTime/LocalDateTime计算,二者适用场景严格区分。

题3:新日期API为什么是线程安全的?

答:所有日期时间对象都是不可变类,修改时间不会改变原对象,只会生成新对象。无共享变量修改操作,多线程并发场景下不会出现数据错乱,天然线程安全。

七、方法引用

方法引用是Lambda表达式的进阶简化语法。当Lambda体仅调用一个已有方法、无任何自定义业务逻辑时,可使用方法引用替代,让代码更简洁紧凑。

本质:复用现有方法逻辑,无额外功能,底层执行原理与Lambda完全一致,仅为语法简化。

方法引用分为四种固定形式,覆盖所有使用场景,可直接套用。

7.1 四种方法引用形式(全示例)

java 复制代码
import java.util.ArrayList;
import java.util.List;

/**
 * 方法引用四种形式完整演示
 * 是Lambda的极简简化写法
 */
public class MethodReferenceDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");

        // 1. 对象::实例方法 (对象调用普通实例方法)
        // Lambda写法:s -> System.out.println(s)
        // 方法引用简化
        list.forEach(System.out::println);

        // 2. 类::静态方法 (类调用静态方法)
        // Lambda写法:n -> Integer.parseInt(n)
        List<String> numStrList = List.of("11", "22", "33");
        numStrList.stream().map(Integer::parseInt).forEach(System.out::println);

        // 3. 类::实例方法 (类名调用实例方法,参数为调用对象)
        // Lambda写法:s -> s.toUpperCase()
        list.stream().map(String::toUpperCase).forEach(System.out::println);

        // 4. 类::new (构造方法引用,创建对象)
        // Lambda写法:() -> new User()
        UserFactory userFactory = User::new;
        User user = userFactory.createUser();
        System.out.println("创建用户对象:" + user);
    }
}

// 自定义工厂函数式接口
@FunctionalInterface
interface UserFactory {
    User createUser();
}

// 自定义实体类
class User {
    @Override
    public String toString() {
        return "用户对象";
    }
}

7.2 四种形式通俗总结

  1. 对象::实例方法:通过实例对象调用普通方法(例:System.out::println)

  2. 类::静态方法:调用工具类静态方法,用于类型转换、数值计算

  3. 类::实例方法:通过类名调用实例方法,Lambda入参为方法调用对象

  4. 类::new:构造方法引用,快速创建对象,适配工厂模式

使用前提:Lambda体仅有一行代码,且仅调用已有方法、无自定义逻辑,才可使用方法引用。

7.3 本节高频面试题(含标准答案)

题1:什么是方法引用?它和Lambda的关系是什么?

答:方法引用是Lambda的极简语法糖,用于简化仅调用已有方法的Lambda场景。无额外业务逻辑时,用方法引用替代Lambda,代码更简洁,底层执行原理与Lambda完全一致。

题2:方法引用有哪四种形式?分别适用什么场景?

答:1.对象::实例方法:对象调用普通方法;2.类::静态方法:调用静态工具方法;3.类::实例方法:参数对象调用实例方法;4.类::new:构造方法创建对象。

题3:所有Lambda表达式都能改成方法引用吗?

答:不能。仅当Lambda方法体只有一行代码,且只是调用已有方法、无任何自定义逻辑、无参数修改时,才能使用方法引用,复杂逻辑无法替换。

八、重复注解与类型注解

Java8之前注解存在两大限制:同一位置无法重复使用同一注解、注解仅能标记类、方法、字段,无法标记变量、泛型等类型。Java8新增重复注解、类型注解,大幅拓展注解使用场景。

主要用于框架参数校验、权限标记、代码检测等场景。

8.1 重复注解(完整自定义实现)

重复注解允许在同一个类、方法、字段上多次使用同一注解。实现必须依赖两个组件:可重复注解、对应容器注解。

java 复制代码
import java.lang.annotation.Repeatable;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * 1. 定义可重复注解
 * Repeatable:标记该注解支持重复使用,绑定容器注解
 */
@Repeatable(RoleContainer.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Role {
    // 注解参数:角色名称
    String value();
}

/**
 * 2. 定义注解容器(存储重复的注解)
 * 必须和可重复注解绑定
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface RoleContainer {
    // 存储多个Role注解
    Role[] value();
}

/**
 * 3. 重复注解使用测试
 */
@Role("管理员")
@Role("超级用户")
@Role("运维人员")
public class RepeatAnnotationDemo {
    public static void main(String[] args) {
        // 获取所有重复注解
        Role[] roles = RepeatAnnotationDemo.class.getAnnotationsByType(Role.class);
        for (Role role : roles) {
            System.out.println("拥有角色:" + role.value());
        }
    }
}

8.2 类型注解

类型注解将注解标记范围拓展至所有类型场景,包含变量类型、方法返回值、泛型、强制类型转换等。

核心作用:实现精细化代码校验与类型检测,是Spring、Hibernate框架参数校验的核心基础。

java 复制代码
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 定义类型注解:可标记任意类型
@Target(ElementType.TYPE_USE)
@Retention(RetentionPolicy.RUNTIME)
@interface CheckNull {
}

// 类型注解使用场景
public class TypeAnnotationDemo {
    // 标记变量类型
    private @CheckNull String name;

    // 标记返回值类型
    public static @CheckNull String getStr() {
        return "测试类型注解";
    }

    public static void main(String[] args) {
        // 标记强制转换类型
        Object obj = "测试";
        String str = (@CheckNull String) obj;
        System.out.println(str);
    }
}

8.3 本节高频面试题(含标准答案)

题1:简述Java8重复注解的实现原理

答:通过@Repeatable注解标记可重复注解,并绑定对应的容器注解。重复使用的注解会统一存入容器注解的数组中,通过反射getAnnotationsByType方法可获取所有重复注解。

题2:类型注解相比传统注解的优势是什么?

答:传统注解仅能标记类、方法、字段;类型注解可标记所有类型场景(变量、返回值、泛型、强转),支持更精细化的代码校验,适配框架参数校验、代码检测场景。

题3:重复注解的容器注解必须满足什么条件?

答:容器注解必须定义value数组,数组类型为可重复注解类型;作用域、生命周期需大于等于可重复注解,保证注解能正常存储和获取。

九、Base64 编码支持

Java8之前无原生Base64工具类,开发者需依赖第三方工具或私有API,存在兼容性、安全性问题。Java8新增 java.util.Base64 原生工具类,线程安全、性能高效,无需额外引入依赖。

常用于字符串、文件、图片的编码传输、参数脱敏场景。

原生提供三种编码模式:基础编码、URL安全编码、MIME编码,适配不同业务场景。

9.1 完整编码解码代码示例

java 复制代码
import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
 * Java8原生Base64编码解码演示
 * 无需第三方依赖,原生线程安全
 */
public class Base64Demo {
    public static void main(String[] args) {
        // 原始字符串
        String content = "Java8新特性学习!123abc";

        // 1. 基础Base64编码
        Base64.Encoder encoder = Base64.getEncoder();
        byte[] encodeBytes = encoder.encode(content.getBytes(StandardCharsets.UTF_8));
        String encodeStr = new String(encodeBytes);
        System.out.println("基础编码结果:" + encodeStr);

        // 2. 基础Base64解码
        Base64.Decoder decoder = Base64.getDecoder();
        byte[] decodeBytes = decoder.decode(encodeStr);
        String decodeStr = new String(decodeBytes, StandardCharsets.UTF_8);
        System.out.println("基础解码结果:" + decodeStr);

        // 3. URL安全编码(解决URL传输特殊字符报错问题)
        String urlEncode = Base64.getUrlEncoder().encodeToString(content.getBytes(StandardCharsets.UTF_8));
        System.out.println("URL安全编码:" + urlEncode);
        String urlDecode = new String(Base64.getUrlDecoder().decode(urlEncode), StandardCharsets.UTF_8);
        System.out.println("URL安全解码:" + urlDecode);

        // 4. MIME编码(适配文件、大文本传输)
        String mimeEncode = Base64.getMimeEncoder().encodeToString(content.getBytes(StandardCharsets.UTF_8));
        System.out.println("MIME编码:" + mimeEncode);
    }
}

9.2 三种编码场景区分

  • 基础编码:普通字符串编码,适用于常规数据传输

  • URL编码:替换+、/等URL特殊非法字符,适配HTTP地址参数传输

  • MIME编码:自动换行处理,适配大文本、文件、邮件传输场景

9.3 本节高频面试题(含标准答案)

题1:Java8 Base64相比旧版本实现有什么优势?

答:Java8提供原生API,无需第三方依赖;底层优化,性能更高;天然线程安全,支持多线程并发;提供基础、URL、MIME三种模式,适配全业务场景,兼容性更强。

题2:Base64编码是加密吗?为什么?

答:不是加密,是编码格式转换。Base64可双向无损解码,无密钥保护,仅用于数据传输脱敏、格式统一,不能保障数据安全,敏感数据需配合加密算法使用。

题3:URL安全Base64编码的作用是什么?

答:标准Base64包含+、/、=等URL特殊字符,传输会出错。URL安全编码会替换非法字符,适配HTTP地址参数传输,避免参数解析异常。

十、Nashorn JavaScript 引擎

Java8废弃老旧的Rhino JS引擎,全新推出Nashorn引擎,大幅提升Java执行JS代码的性能,支持Java与JS双向交互。

核心作用:作为Java内置JS解释器,可直接执行JS代码、调用JS方法、双向传递参数,实现简单的跨脚本交互。

版本重点:Nashorn为过渡特性,Java8新增、Java11标记过期、Java15正式移除,生产环境不建议重度使用。

10.1 简单调用完整示例

java 复制代码
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

/**
 * Nashorn JavaScript引擎演示
 * Java调用JS代码、执行JS方法
 */
public class NashornDemo {
    public static void main(String[] args) {
        // 1. 获取Nashorn JS引擎
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine nashorn = manager.getEngineByName("nashorn");

        try {
            // 2. 直接执行JS单行代码
            nashorn.eval("var a = 10; var b = 20; print('JS计算结果:' + (a+b))");

            // 3. 执行JS自定义函数
            String jsFunc = "function add(x,y){return x+y;}";
            nashorn.eval(jsFunc);

            // 4. Java调用JS函数并传参、获取返回值
            Invocable invocable = (Invocable) nashorn;
            Object result = invocable.invokeFunction("add", 100, 200);
            System.out.println("Java调用JS函数结果:" + result);

            // 5. Java向JS传递变量
            nashorn.put("javaMsg", "Java传递给JS的参数");
            nashorn.eval("print(javaMsg)");

        } catch (ScriptException | NoSuchMethodException e) {
            e.printStackTrace();
        }
    }
}

10.2 核心应用场景说明

  1. 动态脚本执行:业务规则通过JS脚本配置,无需重启服务即可更新逻辑

  2. 脚本复用:前后端通用JS校验规则,后端可直接复用,避免校验逻辑不一致

  3. 动态计算:适配自定义公式、表达式动态解析计算场景

10.3 本节高频面试题(含标准答案)

题1:Nashorn引擎的作用是什么?相比Rhino有什么优势?

答:Nashorn是Java8内置JS引擎,用于Java调用执行JS代码。相比旧Rhino引擎,执行速度更快、兼容性更好、语法贴合现代JS,大幅提升跨脚本交互性能。

题2:Nashorn引擎的版本迭代状态是什么?

答:Java8新增、替代Rhino;Java11标记为过时不推荐使用;Java15正式彻底移除。属于Java8过渡特性,生产环境不建议重度依赖。

题3:Java通过Nashorn可以实现哪些JS交互操作?

答:可直接执行JS代码、调用JS自定义函数、Java向JS传递变量、获取JS执行返回值,实现Java与JavaScript双向数据交互和脚本调用。

相关推荐
冰心孤城2 小时前
Excel: xls与xlsx格式转换排坑指南
java·前端·excel
自强的小白2 小时前
maven高级(聚合和私服)以及私服的上传和下载
java·maven
唐青枫2 小时前
Java ReentrantLock 实战详解:比 synchronized 更灵活的可重入锁
java
ShiXZ2133 小时前
Java 8 Stream API 实用技巧详解:从入门到精通
java·开发语言
C++、Java和Python的菜鸟3 小时前
第9章 后端Web进阶(AOP)
java·开发语言·前端
东北赵四3 小时前
关于Java泛型的知识点及其相关面试题
java·windows·python
姓蔡小朋友3 小时前
Java线程并发
java·开发语言·python
执笔画流年呀4 小时前
⾃动化测试常⽤函数
java·dubbo
疯狂打码的少年4 小时前
【面向对象】UML概述(基本构造块:事物/关系/图)
java