Java基础学习——第十六章 Java8新特性
Java基础学习——第十六章 Java8 新特性
Java8(JDK8.0)较 JDK7.0 有很多变化或者说是优化,比如 interface 里可以有静态方法和默认方法,并且可以有方法体,这一点就颠覆了之前的认知;java.util.HashMap
数据结构里增加了红黑树;还有众所周知的 Lambda 表达式等等
一、Interface
interface 的设计初衷是面向抽象,提高扩展性。这也留有一点遗憾,Interface 修改的时候,实现它的类也必须跟着改。
为了解决接口的修改与现有的实现不兼容的问题。新 interface 的方法可以用default
或 static
修饰,这样就可以有方法体,实现类也不必重写此方法。
一个 interface 中可以有多个方法被它们修饰,这 2 个修饰符的区别主要也是普通方法和静态方法的区别。
default
修饰的方法,是普通实例方法,可以用this
调用,可以被子类继承、重写。static
修饰的方法,使用上和一般类静态方法一样。但它不能被子类继承,只能用Interface
调用。
我们来看一个实际的例子。
public interface InterfaceNew {static void sm() {System.out.println("interface提供的方式实现");}static void sm2() {System.out.println("interface提供的方式实现");}default void def() {System.out.println("interface default方法");}default void def2() {System.out.println("interface default2方法");}//须要实现类重写void f();
}public interface InterfaceNew1 {default void def() {System.out.println("InterfaceNew1 default方法");}
}
如果有一个类既实现了 InterfaceNew
接口又实现了 InterfaceNew1
接口,它们都有def()
,并且 InterfaceNew
接口和 InterfaceNew1
接口没有继承关系的话,这时就必须重写def()
。不然的话,编译的时候就会报错。
public class InterfaceNewImpl implements InterfaceNew , InterfaceNew1{public static void main(String[] args) {InterfaceNewImpl interfaceNew = new InterfaceNewImpl();interfaceNew.def();}@Overridepublic void def() {InterfaceNew1.super.def();}@Overridepublic void f() {}
}
在 Java 8 中,接口和抽象类有什么区别的?
interface 和 class 的区别主要有:
- 接口多实现,类单继承
- 接口的方法是 public abstract 修饰,变量是 public static final 修饰。 abstract class 可以用其他修饰符
interface 的方法是更像是一个扩展插件。而 abstract class 的方法是要继承的。
开始我们也提到,interface 新增default
和static
修饰的方法,为了解决接口的修改与现有的实现类不兼容的问题,并不是为了要替代abstract class
。在使用上,该用 abstract class 的地方还是要用 abstract class,不要因为 interface 的新特性而将之替换。
记住接口永远和类不一样
二、函数式接口:functional interface
定义:也称 SAM 接口,即 Single Abstract Method interfaces,有且只有一个抽象方法,但可以有多个非抽象方法的接口。
在 java 8 中专门有一个包放函数式接口java.util.function
,该包下的所有接口都有 @FunctionalInterface
注解。
在其他包中也有函数式接口,其中一些没有@FunctionalInterface
注解,但是只要符合函数式接口的定义就是函数式接口,与是否有@FunctionalInterface
注解无关,注解只是在编译时起到强制规范定义的 作用。其在 Lambda 表达式中有广泛的应用。
三、Lambda 表达式
使用 Lambda 表达式可以使代码变的更加简洁紧凑。让 java 也能支持简单的函数式编程。
Lambda 表达式是一个匿名函数,java 8 允许把函数作为参数传递进方法中
Lambda 表达式的本质:整个 Lambda 表达式代表一个函数式接口的对象
使用情境:当需要对一个函数式接口实例化时,或方法形参中需要函数式接口的对象时,我们使用Lambda表达式来代替匿名实现类
1. 语法格式
- **
->
:**Lambda 操作符或箭头操作符 - 左侧:Lambda 表达式的形参列表——接口中抽象方法的形参列表
- **右侧:**Lambda 体——实现的抽象方法的方法体,也即 Lambda 表达式要执行的功能
(parameters) -> {statements;}; 或
(parameters) -> expression;
语法格式一:抽象方法没有形参,无返回值
Runnable r = () -> {System.out.println("It's a lambda function!");};
语法格式二:抽象方法有一个参数,无返回值
Consumer<String> con = (String str) -> {System.out.println(str);};
语法格式三:数据类型可以省略,因为可由编译器推断得出,称为“类型推断”
Consumer<String> con = (str) -> {System.out.println(str);};
语法格式四:Lambda 操作符左边若只有一个参数时, 参数的小括号可以省略
Consumer<String> con = str -> {System.out.println(str);};
语法格式五:抽象方法需要两个或以上的参数,多条执行语句,并且可以有返回值(标准写法)
Comparator<Integer> com = (o1, o2) -> {System.out.println("实现函数式接口方法");return Integer.compare(o1, o2);
};
语法格式六:当 Lambda 体只有一条执行语句(可能是 return 语句), return与大括号都可以省略
Comparator<Integer> com = (o1, o2) -> Integer.compare(o1, o2);
1.1 总结
- **左侧:**① Lambda 表达式的形参列表的参数类型可以省略(类型推断);② 如果只有一个形参,则小括号可以省略
- **右侧:**如果 Lambda 体只有一条执行语句(可能是 return 语句),则 return 与大括号都可以省略
2. Lambda 实战
2.1 替代匿名内部类
过去给方法传动态参数的唯一方法是使用内部类(匿名实现类)。比如
1.Runnable
接口
//Runnable接口的匿名实现类的匿名对象
//不使用lambda表达式
new Thread(new Runnable() {@Overridepublic void run() {System.out.println("The runable now is using!");}
}).start();//使用lambda表达式
new Thread(() -> System.out.println("It's a lambda function!")).start();
2.Comparator
接口
List<Integer> list = Arrays.asList(1, 2, 3);//不使用lambda表达式
Collections.sort(list, new Comparator<Integer>() {@Overridepublic int compare(Integer o1, Integer o2) {return Integer.compare(o1, o2);}
});//使用lambda表达式
Collections.sort(list, (Integer o1, Integer o2) -> Integer.compare(o1, o2));//分解开
Comparator<Integer> comperator = (Integer o1, Integer o2) -> Integer.compare(o1, o2);
Collections.sort(list, comperator);
3.Listener
接口
JButton button = new JButton();//不使用lambda表达式
button.addItemListener(new ItemListener() {@Override
public void itemStateChanged(ItemEvent e) {e.getItem();
}
});//lambda
button.addItemListener(e -> e.getItem());
4.自定义接口
上面的 3 个例子是我们在开发过程中最常见的,从中也能体会到 Lambda 带来的便捷与清爽。它只保留实际用到的代码,把无用代码全部省略。那它对接口有没有要求呢?我们发现这些匿名内部类只重写了接口的一个方法,当然也只有一个方法须要重写。这就是我们上文提到的函数式接口,也就是说只要方法的参数是函数式接口的对象,就可以使用 Lambda 表达式。
@FunctionalInterface
public interface Comparator<T>{}@FunctionalInterface
public interface Runnable{}
我们自定义一个函数式接口
@FunctionalInterface
public interface LambdaInterface {void f();
}
//使用
public class LambdaClass {public static void forEg() {lambdaInterfaceDemo(()-> System.out.println("自定义函数式接口"));}//函数式接口参数static void lambdaInterfaceDemo(LambdaInterface i){System.out.println(i);}
}
2.2 集合迭代:forEach()方法
void lamndaFor() {List<String> strings = Arrays.asList("1", "2", "3");//传统foreachfor (String s : strings) {System.out.println(s);}//Collection:public void forEach(Consumer<? super E> action)//lambdastrings.forEach((s) -> System.out.println(s));//方法引用strings.forEach(System.out::println);//map:default void forEach(BiConsumer<? super K, ? super V> action)Map<Integer, String> map = new HashMap<>();map.forEach((k,v)->System.out.println(v));
}
2.3 方法引用
2.3.1 方法引用
方法引用是一种更加紧凑,易读的 Lambda 表达式。即方法引用就是 Lambda 表达式,本质上代表一个函数式接口的对象
- **情境:**对于某个 Lambda 表达式,当其 Lambda 体仅调用了一个已存在的方法,而不执行任何其它操作,可以考虑使用方法引用
- 要求:函数式接口的抽象方法的参数列表和返回值类型,必须与方法引用的方法的参数列表和返回值类型保持一致(针对情况1和2)
- 格式:使用操作符
::
将类名(或对象引用)与方法名分隔开来,整个表达式代表一个函数式接口的对象- 注意:
::
右边的方法必须是左边的类或对象所在类中声明的方法
- 注意:
类名(或对象引用)::方法名
- 主要使用情况:
- 情况1:
对象引用::实例(非静态)方法
- 情况2:
类名::静态方法名
- 情况3:
类名::实例(非静态)方法
- 情况1:
public class MethodRefTest {//情况一:对象引用::实例(非静态)方法//Consumer<T>中的void accept(T t)//PrintStream中的void println(Xxx x)@Testpublic void test1() {//Lambda表达式Consumer<String> con1 = str -> System.out.println(str);con1.accept("Beijing");//方法引用Consumer<String> con2 = System.out::println;con2.accept("Shanghai");}//Supplier<T>中的T get()//Employee中的String getName()@Testpublic void test2() {Employee emp = new Employee(1001, "Tom", 23, 5600);//Lambda表达式Supplier<String> sup1 = () -> emp.getName();System.out.println(sup1.get());//方法引用Supplier<String> sup2 = emp::getName;System.out.println(sup2.get());}//情况二:类名::静态方法名//Comparator<T>中的int compare(T t1,T t2)//Integer中的int compare(T t1,T t2)@Testpublic void test3() {//Lambda表达式Comparator<Integer> com1 = (t1, t2) -> Integer.compare(t1, t2);System.out.println(com1.compare(12, 3));//方法引用Comparator<Integer> com2 = Integer::compare;System.out.println(com2.compare(12, 3));}//Function<T, R>中的R apply(T t)//Math中的Long round(Double d)@Testpublic void test4() {//Lambda表达式Function<Double, Long> func1 = d -> Math.round(d);System.out.println(func1.apply(2.4));//方法引用Function<Double, Long> func2 = Math::round;System.out.println(func2.apply(2.4));}//情况三:类名::实例(非静态)方法//Comparator<T>中的int compare(T t1,T t2)//String中的int t1.compareTo(t2)@Testpublic void test5() {//Lambda表达式Comparator<String> com1 = (t1, t2) -> t1.compareTo(t2);System.out.println(com1.compare("123", "456"));//方法引用Comparator<String> com2 = String::compareTo;System.out.println(com2.compare("123", "456"));}//BiPredicate<T, U>中的boolean test(T t, U u);//String中的boolean t1.equals(t2)@Testpublic void test6() {//Lambda表达式BiPredicate<String, String> pre1 = (t1, t2) -> t1.equals(t2);System.out.println(pre1.test("123", "123"));//方法引用BiPredicate<String, String> pre2 = String::equals;System.out.println(pre2.test("123", "123"));}//Function<T, R>中的R apply(T t)//Employee中的String getName();@Testpublic void test7() {Employee emp = new Employee(1001, "Tom", 23, 5600);//Lambda表达式Function<Employee, String> func1 = e -> e.getName();System.out.println(func1.apply(emp));//方法引用Function<Employee, String> func2 = Employee::getName;System.out.println(func2.apply(emp));}
}
2.3.2 构造器引用
**情境:**对于某个 Lambda 表达式,当其 Lambda 体仅调用某个类的构造器来new对象,而不执行其它操作,可以使用构造器引用
要求:函数式接口的抽象方法的参数列表与构造器的参数列表一致,且抽象方法的返回值即为构造器对应类的对象
格式:使用操作符
::
将类名与new分隔开来,整个表达式代表一个函数式接口的对象
类名::new
public class ConstructorRefTest {//构造器引用//Supplier<T>中的T get()@Testpublic void test1(){//匿名实现类Supplier<Employee> sup = new Supplier<Employee>() {@Overridepublic Employee get() {return new Employee();}};//Lambda表达式Supplier<Employee> sup1 = () -> new Employee();//构造器引用Supplier<Employee> sup2 = Employee::new;}//Function<T, R>中的R apply(T t)@Testpublic void test2(){//Lambda表达式Function<Integer, Employee> func1 = id -> new Employee(id);//构造器引用Function<Integer, Employee> func2 = Employee::new;System.out.println(func1.apply(1001).getId());}//BiFunction<T, U, R>中的R apply(T t,U u)@Testpublic void test3(){//Lambda表达式BiFunction<Integer, String, Employee> func1 = (id, name) -> new Employee(id, name);//构造器引用BiFunction<Integer, String, Employee> func2 = Employee::new;System.out.println(func2.apply(1001, "Tom"));}
}
2.3.3 数组引用
格式:
Xxx[]::new
可以将数组看成是一个特殊的类,则数组引用的写法与构造器引用类似
public class ArrayRefTest {//数组引用//Function<T, R>中的R apply(T t)@Testpublic void test4() {//Lambda表达式Function<Integer, String[]> func1 = length -> new String[length];String[] arr1 = func1.apply(5);System.out.println(Arrays.toString(arr1));//数组引用Function<Integer, String[]> func2 = String[]::new;String[] arr2 = func2.apply(5);System.out.println(Arrays.toString(arr2));}
}
2.4 访问变量
int i = 0;
Collections.sort(strings, (Integer o1, Integer o2) -> o1 - i);
//i =3;
lambda 表达式可以引用外边变量,但是该变量默认拥有 final 属性,不能被修改,如果修改,编译时就报错。
四、Stream
1. Stream API的概述
Stream 关注的是对数据的运算,与CPU打交道;Collection 关注的是数据的存储,与内存打交道
- java 新增了
java.util.stream
包,它和之前的流大同小异。之前接触最多的是资源流,比如java.io.FileInputStream
,通过流把文件从一个地方输入到另一个地方,它只是内容搬运工,对文件内容不做任何增删改查 Stream
是数据渠道,用于操作数据源(Collection
、Array
等)所生成的元素序列。它可以检索(Retrieve)和逻辑处理集合数据、包括过滤、切片、映射、排序、归约等操作,类似 Sql 语句Stream
的特性:Stream
不保存数据Stream
不改变数据源。相反,他们会返回一个持有结果的新Stream
- 通过链式编程,使得
Stream
可以方便地对遍历处理后的数据进行再处理 Stream
操作是延迟执行的,只有执行终止操作时,才会执行中间操作链,并返回结果- 一个
Stream
只能经历一次 实例化 —> 中间操作 —> 终止操作 的过程,即当终止操作执行后该stream
就关闭了,此时再通过这个stream
调用方法会报异常 Stream
的方法参数都是函数式接口类型,所以一般和 Lambda 配合使用
2. 操作Stream的三个步骤
- **实例化
Stream
类对象:**根据某个数据源(如Collection
、Array
等),获取一个流 - **中间操作(链式编程):**一系列中间操作,声明如何对数据源的数据进行处理
- **终止操作:**一旦执行终止操作,才会执行中间操作链,并返回结果。之后,不会再被使用
3. 实例化Stream对象
3.1 方式一:通过集合
default Stream<E> stream()
:返回一个串行流(顺序流)default Stream<E> parallelStream()
:返回一个并行流
@Test
public void test1() {List<Employee> employees = EmployeeData.getEmployees();//1. default Stream<E> stream():返回一个串行流(顺序流)Stream<Employee> stream = employees.stream();//2. default Stream<E> parallelStream():返回一个并行流Stream<Employee> employeeStream = employees.parallelStream();
}
3.2 方式二:通过数组
- Java8 中 Arrays 工具类的静态方法 stream() 可以获取数组流:
static <T> Stream<T> stream(T[] array)
@Test
public void test2() {int[] arr = new int[]{1, 2, 3};//static <T> Stream<T> stream(T[] array):返回一个流IntStream stream = Arrays.stream(arr);Employee[] arr1 = new Employee[]{new Employee(), new Employee()};Stream<Employee> stream1 = Arrays.stream(arr1);
}
3.3 方式三:通过Stream类的静态方法of()
Stream类的静态方法
of()
,通过显示值创建一个流,可以接收任意数量的参数:public static<T> Stream<T> of(T... values)
@Test
public void test3() {//public static<T> Stream<T> of(T... values):返回一个流Stream<Integer> integerStream = Stream.of(1, 2, 3);
}
3.4 方式四:创建无限流(造数据)
Stream类的静态方法
iterate()
或generate()
,创建无限流:- 迭代:
public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
- 迭代:
生成:
public static<T> Stream<T> generate(Supplier<T> s)
@Test
public void test4() {//1. 迭代:public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)//遍历前10个偶数Stream.iterate(0, t -> t + 2).limit(6).forEach(System.out::println);//2. 生成:public static<T> Stream<T> generate(Supplier<T> s)Stream.generate(Math::random).limit(6).forEach(System.out::println);
}
4. Stream的中间操作
- 多个中间操作可以连接起来(链式编程)形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为**“惰性求值”**
- 中间操作涉及到的方法的返回值类型仍为
Stream
4.1 筛选与切片
Stream<T> filter(Predicate<? super T> predicate)
:从流中筛选某些元素,保留满足指定判定条件的元素Stream<T> limit(long maxSize)
:截断流,返回一个元素数量不超过maxSize的流,按照数据源的顺序截断Stream<T> skip(long n)
:返回一个跳过了前n个元素的流。若原始流中的元素不足n个,则返回一个空流。与limit(n)互补Stream<T> distinct()
:通过流所生成元素的hashCode()
和equals()
去除重复元素,返回一个没有重复元素的流
@Test
public void test1() {List<Employee> list = EmployeeData.getEmployees();Stream<Employee> stream = list.stream();//1. Stream<T> filter(Predicate<? super T> predicate):从流中筛选某些元素,保留满足指定判定条件的元素//筛选员工表中薪资大于7000的员工信息// stream.filter(new Predicate<Employee>() {// @Override// public boolean test(Employee e) {// return e.getSalary() > 7000;// }// }).forEach(System.out::println);//也可以写成Lambda表达式stream.filter(e -> e.getSalary() > 7000).forEach(System.out::println); //forEach()是一个终止操作//2. Stream<T> limit(long maxSize):截断流,返回一个元素数量不超过maxSize的流,按照数据源的顺序截断//一个 `Stream` 只能进行一次完整的操作,即当终止操作执行后就关闭了,此时再使用这个 `stream` 会报错//stream.limit(3).forEach(System.out::println);//此时应当重新创建一个Stream对象list.stream().limit(3).forEach(System.out::println);//3. Stream<T> skip(long n):返回一个跳过了前n个元素的流。若流中元素不足n个,则返回一个空流。与limit(n)互补list.stream().skip(3).forEach(System.out::println);//4. Stream<T> distinct():通过流所生成元素的 hashCode() 和 equals() 去除重复元素,返回一个没有重复元素的流list.stream().distinct().forEach(System.out::println);
}
4.2 映射
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
:接收一个函数(通常写成Lambda表达式)作为参数,该函数会被应用到流的每个元素上,映射成新的元素。返回映射操作后的流<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
:接收一个函数(通常写成Lambda表达式)作为参数,将流中的每个元素都换成另一个流,然后把所有流连接成一个流
@Test
public void tese2() {List<String> list = Arrays.asList("aa", "bb", "cc", "dd");//1. <R> Stream<R> map(Function<? super T, ? extends R> mapper)//接收一个函数(通常写成Lambda表达式)作为参数,该函数会被应用到流的每个元素上,映射成新的元素。返回映射操作后的流list.stream().map(str -> str.toUpperCase()).forEach(System.out::println);//练习1:获取姓名长度大于3的员工的姓名List<Employee> employees = EmployeeData.getEmployees();//先调用map()将流中所有元素(Employee对象)映射为对应的name属性,然后调用filter()筛选仅保留名字长度大于3的员工//Stream的中间操作采用链式编程,最后通过forEach()执行终止操作//Stream<String> nameStream = employees.stream().map(employee -> employee.getName());//nameStream.filter(name -> name.length() > 3).forEach(System.out::println);//写成链式,并使用方法引用employees.stream().map(Employee::getName).filter(name -> name.length() > 3).forEach(System.out::println);//练习2:将list中的每个字符串元素中的每个字符单独打印出来//使用map()Stream<Stream<Character>> streamStream = list.stream().map(StreamAPITest1::fromStringToStream); //Stream里面套了StreamstreamStream.forEach(s -> s.forEach(System.out::println));//2. <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)//接收一个函数(通常写成Lambda表达式)作为参数,将流中的每个元素都换成另一个流,然后把所有流连接成一个流Stream<Character> characterStream = list.stream().flatMap(StreamAPITest1::fromStringToStream);characterStream.forEach(System.out::println);
}//将字符串中的多个字符构成的集合转换成对应的Stream对象
public static Stream<Character> fromStringToStream(String str) {ArrayList<Character> list = new ArrayList<>();for (Character c : str.toCharArray()) {list.add(c);}return list.stream();
}
4.3 排序
Stream<T> sorted()
:返回一个新的流,其中元素按自然顺序排序(自然排序)。元素所在类必须实现Comparable接口Stream<T> sorted(Comparator com)
:返回一个新的流,其中元素按定制顺序排序(定制排序)。需要传入Comparator接口的实现类对象(使用Lambda表达式)
@Test
public void test4() {List<Integer> list = Arrays.asList(12, 43, 65, 34, 87, 0, -98, 7);List<Employee> employees = EmployeeData.getEmployees();//1. `Stream<T> sorted()`:返回一个新的流,其中元素按自然顺序排序(自然排序)。元素所在类必须实现Comparable接口//此处按照Integer内部定义的compareTo方法进行自然排序list.stream().sorted().forEach(System.out::println);//2. `Stream<T> sorted(Comparator com)`:返回一个新的流,其中元素按定制顺序排序(定制排序)employees.stream().sorted((e1, e2) -> Integer.compare(e1.getAge(), e2.getAge())).forEach(System.out::println);
}
5. Stream的终止操作
- 终止操作会从流的流水线生成结果
- 终止操作涉及到的方法的返回值类型可以是任何不是
Stream
的类型,例如:List、Integer,甚至是 void - 流进行了终止操作后,不能再次使用
5.1 匹配与查找
boolean allMatch(Predicate<? super T> predicate)
:检查当前流中是否所有元素都满足指定判定条件boolean anyMatch(Predicate<? super T> predicate)
:检查当前流中是否至少有一个元素满足指定判定条件boolean noneMatch(Predicate<? super T> predicate)
:检查当前流中是否没有元素满足指定判定条件Optional<T> findFirst()
:返回当前流的第一个元素Optional<T> findAny()
:返回当前流中的任意元素long count()
:返回当前流中元素总数Optional<T> max(Comparator<? super T> comparator)
:返回当前流中按照定制排序顺序的最大元素。需要传入Comparator接口的实现类对象(使用Lambda表达式)Optional<T> min(Comparator<? super T> comparator)
:返回当前流中按照定制排序顺序的最小元素。需要传入Comparator接口的实现类对象(使用Lambda表达式)void forEach(Consumer<? super T> action)
:内部迭代- 使用 Collection 接口需要自行使用迭代器迭代,称为外部迭代。相反,Stream API 使用内部迭代——它帮你把迭代做了
@Test
public void test1() {List<Employee> employees = EmployeeData.getEmployees();//1. boolean allMatch(Predicate<? super T> predicate):检查是否所有元素都满足指定判定条件//练习1:是否所有员工的年龄都大于18岁boolean allMatch = employees.stream().allMatch(employee -> employee.getAge() > 18);System.out.println(allMatch); //false//2. boolean anyMatch(Predicate<? super T> predicate):检查是否至少有一个元素满足指定判定条件//练习2:是否存在员工的工资大于10000boolean anyMatch = employees.stream().anyMatch(employee -> employee.getSalary() > 10000);System.out.println(anyMatch); //false//3. boolean noneMatch(Predicate<? super T> predicate):检查是否没有元素满足指定判定条件//练习3:是否没有员工姓雷boolean noneMatch = employees.stream().noneMatch(employee -> employee.getName().startsWith("雷"));System.out.println(noneMatch); //false//4. Optional<T> findFirst():返回第一个元素//练习4:返回年龄最小的员工的信息Optional<Employee> first = employees.stream().sorted((employee1, employee2) -> Integer.compare(employee1.getAge(), employee2.getAge())).findFirst();System.out.println(first);//5. Optional<T> findAny():返回当前流中的任意元素Optional<Employee> any = employees.parallelStream().findAny();System.out.println(any);//6. long count():返回当前流中元素总数//练习5:返回工资大于5000的员工个数long count = employees.stream().filter(employee -> employee.getSalary() > 5000).count();System.out.println(count);//7.Optional<T> max(Comparator<? super T> comparator):返回当前流中按照定制排序顺序的最大元素//练习6:返回所有员工的最高工资Optional<Double> maxSalary = employees.stream().map(employee -> employee.getSalary()).max((s1, s2) -> Double.compare(s1, s2));System.out.println(maxSalary);//8. Optional<T> min(Comparator<? super T> comparator):返回当前流中按照定制排序顺序的最小元素//练习7:返回工资最低的员工Optional<Employee> min = employees.stream().min((employee1, employee2) -> Double.compare(employee1.getSalary(), employee2.getSalary()));System.out.println(min);//9. void forEach(Consumer<? super T> action):内部迭代employees.stream().forEach(System.out::println);//使用集合的遍历操作employees.forEach(System.out::println);
}
5.2 归约
T reduce(T identity, BinaryOperator<T> accumulator)
:将流中元素反复结合起来,得到一个值,返回值类型为泛型类型T
。identity表示初始值Optional<T> reduce(BinaryOperator<T> accumulator)
:将流中元素反复结合起来,得到一个值,返回值类型为Optional<T>
@Test
public void test2() {//1. reduce(T iden, BinaryOperator b)//练习1:计算1-10的自然数的和List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);Integer sum = list.stream().reduce(0, Integer::sum);System.out.println(sum);//2. Optional<T> reduce(BinaryOperator<T> accumulator)//练习2:计算所有员工的工资总和List<Employee> employees = EmployeeData.getEmployees();//Optional<Double> salarySum = employees.stream().map(Employee::getSalary).reduce(Double::sum);Optional<Double> salarySum = employees.stream().map(Employee::getSalary).reduce((s1, s2) -> s1 + s2);System.out.println(salarySum);
}
5.3 收集
<R, A> R collect(Collector<? super T, A, R> collector)
:将流转换为其他形式。接收一个 Collector 接口的实现类对象,用于给Stream中元素做汇总的方法- Collector 接口中方法的实现决定了如何对流执行收集的操作(如收集到 List、 Set、Map)
- 另外, Collectors 工具类中提供了很多静态方法,可以方便地创建常见收集器实例,常见的有:
Collectors.toList()
、Collectors.toSet()
、Collectors.toCollection()
等
@Test
public void test3() {List<Employee> employees = EmployeeData.getEmployees();//<R, A> R collect(Collector<? super T, A, R> collector)://练习1:查找工资大于6000的员工,结果返回为一个List或SetList<Employee> employeeList = employees.stream().filter(employee -> employee.getSalary() > 6000).collect(Collectors.toList());employeeList.forEach(System.out::println);Set<Employee> employeeSet = employees.stream().filter(employee -> employee.getSalary() > 6000).collect(Collectors.toSet());employeeSet.forEach(System.out::println);
}
6. Stream的延迟执行
**在执行中间操作(返回值类型为 Stream
的方法)时,并不立即执行,而是等执行终止操作(返回值类型为非 Stream
的方法)后才执行。**因为拿到 Stream
并不能直接用,而是需要处理成一个常规类型。这里的 Stream
可以想象成是二进制流,拿到也看不懂。
我们下面分解一下 filter
方法。
@Test
public void laziness(){List<String> strings = Arrays.asList("abc", "def", "gkh", "abc");Stream<Integer> stream = strings.stream().filter(new Predicate() {@Overridepublic boolean test(Object o) {System.out.println("Predicate.test 执行");return true;}});System.out.println("count 执行");stream.count();
}
/*-------执行结果--------*/
count 执行
Predicate.test 执行
Predicate.test 执行
Predicate.test 执行
Predicate.test 执行
按执行顺序应该是先打印 4 次「Predicate.test
执行」,再打印「count
执行」。实际结果恰恰相反。说明 filter 中的方法并没有立刻执行,而是等调用count()
方法后才执行。
上面都是串行 Stream
的实例。并行 parallelStream
在使用方法上和串行一样。主要区别是 parallelStream
可多线程执行,是基于 ForkJoin
框架实现的,有时间大家可以了解一下 ForkJoin
框架和 ForkJoinPool
。这里可以简单的理解它是通过线程池来实现的,这样就会涉及到线程安全,线程消耗等问题。下面我们通过代码来体验一下并行流的多线程执行。
@Test
public void parallelStreamTest(){List<Integer> numbers = Arrays.asList(1, 2, 5, 4);numbers.parallelStream() .forEach(num->System.out.println(Thread.currentThread().getName()+">>"+num));
}
//执行结果
main>>5
ForkJoinPool.commonPool-worker-2>>4
ForkJoinPool.commonPool-worker-11>>1
ForkJoinPool.commonPool-worker-9>>2
从结果中我们看到,forEach
用到的是多线程。
五、Optional
在阿里巴巴开发手册关于 Optional 的介绍中这样写到:
防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:
1) 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE。
反例:public int f() { return Integer 对象}, 如果为 null,自动拆箱抛 NPE。
2) 数据库的查询结果可能为 null。
3) 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
4) 远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
5) 对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。
6) 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。
正例:使用 JDK8 的 Optional 类来防止 NPE 问题。
他建议使用 Optional
解决 NPE(java.lang.NullPointerException
)问题,它就是为 NPE 而生的,其中可以包含空值或非空值。下面我们通过源码逐步揭开 Optional
的红盖头。
假设有一个 Zoo
类,里面有个属性 Dog
,需求要获取 Dog
的 age
。
class Zoo {private Dog dog;
}class Dog {private int age;
}
传统解决 NPE 的办法如下:
Zoo zoo = getZoo();
if(zoo != null){Dog dog = zoo.getDog();if(dog != null){int age = dog.getAge();System.out.println(age);}
}
层层判断对象非空,有人说这种方式很丑陋不优雅,我并不这么认为。反而觉得很整洁,易读,易懂。你们觉得呢?
Optional
是这样的实现的:
Optional.ofNullable(zoo).map(o -> o.getDog()).map(d -> d.getAge()).ifPresent(age ->System.out.println(age)
);
是不是简洁了很多呢?
1. 理解Optional是一个容器类
Optional<T>
类(java.util.Optional) 是一个容器类,它可以保存类型T的值,代表这个值存在。或者仅仅保存null,表示这个值不存在。原来用 null 表示一个值不存在,现在 Optional 可以更好的表达这个概念。并且可以避免空指针异常
2. 如何创建一个 Optional 对象
上例中Optional.ofNullable
是其中一种创建 Optional 对象的静态方法。我们先看一下它的含义和其他创建 Optional 的源码方法。
/**
* Common instance for {@code empty()}. 全局EMPTY对象
*/
private static final Optional<?> EMPTY = new Optional<>();/**
* Optional维护的值
*/
private final T value;/**
* 如果value是null就返回EMPTY,否则就返回of(T)
*/
public static <T> Optional<T> ofNullable(T value) {return value == null ? empty() : of(value);
}
/**
* 返回 EMPTY 对象
*/
public static<T> Optional<T> empty() {Optional<T> t = (Optional<T>) EMPTY;return t;
}
/**
* 返回Optional对象
*/
public static <T> Optional<T> of(T value) {return new Optional<>(value);
}
/**
* 私有构造方法,给value赋值
*/
private Optional(T value) {this.value = Objects.requireNonNull(value);
}
/**
* 所以如果of(T value) 的value是null,会抛出NullPointerException异常,这样貌似就没处理NPE问题
*/
public static <T> T requireNonNull(T obj) {if (obj == null)throw new NullPointerException();return obj;
}
ofNullable
方法和of
方法唯一区别就是当 value 为 null 时,ofNullable
返回的是EMPTY
,of 会抛出 NullPointerException
异常。如果需要把 NullPointerException
暴漏出来就用 of
,否则就用 ofNullable
。
3. map()相关方法
/**
* 如果value为null,返回EMPTY,否则返回Optional封装的参数值
*/
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {Objects.requireNonNull(mapper);if (!isPresent())return empty();else {return Optional.ofNullable(mapper.apply(value));}
}
/**
* 如果value为null,返回EMPTY,否则返回Optional封装的参数值,如果参数值返回null会抛 NullPointerException
*/
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {Objects.requireNonNull(mapper);if (!isPresent())return empty();else {return Objects.requireNonNull(mapper.apply(value));}
}
map()
和 flatMap()
有什么区别的?
1.参数不一样,map
的参数上面看到过,flatMap
的参数是这样
class ZooFlat {private DogFlat dog = new DogFlat();public DogFlat getDog() {return dog;}}class DogFlat {private int age = 1;public Optional<Integer> getAge() {return Optional.ofNullable(age);}
}ZooFlat zooFlat = new ZooFlat();
Optional.ofNullable(zooFlat).map(o -> o.getDog()).flatMap(d -> d.getAge()).ifPresent(age ->System.out.println(age)
);
2.flatMap()
参数返回值如果是 null 会抛 NullPointerException
,而 map()
返回EMPTY
。
4. 判断 value 是否为 null
/**
* value是否为null
*/
public boolean isPresent() {return value != null;
}
/**
* 如果value不为null,执行consumer接口的实现类实现的accept()方法
*/
public void ifPresent(Consumer<? super T> consumer) {if (value != null)consumer.accept(value);
}
5. 获取Optional容器中的对象 value
/**
* Return the value if present, otherwise invoke {@code other} and return
* the result of that invocation.
* 如果value != null 返回value,否则返回other的执行结果
*/
public T orElseGet(Supplier<? extends T> other) {return value != null ? value : other.get();
}/**
* 如果value != null 返回value,否则返回T
*/
public T orElse(T other) {return value != null ? value : other;
}/**
* 如果value != null 返回value,否则抛出参数返回的异常
*/
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {if (value != null) {return value;} else {throw exceptionSupplier.get();}
}
/**
* value为null抛出NoSuchElementException,不为空返回value。
*/
public T get() {if (value == null) {throw new NoSuchElementException("No value present");}return value;
}
6. 过滤值
/**
* 1. 如果是empty返回empty
* 2. predicate.test(value)==true 返回this,否则返回empty
*/
public Optional<T> filter(Predicate<? super T> predicate) {Objects.requireNonNull(predicate);if (!isPresent())return this;elsereturn predicate.test(value) ? this : empty();
}
7. 小结
看完 Optional
源码,Optional
的方法真的非常简单,值得注意的是如果坚决不想看见 NPE
,就不要用 of()
、 get()
、flatMap(..)
。最后再综合用一下 Optional
的高频方法。
Optional.ofNullable(zoo).map(o -> o.getDog()).map(d -> d.getAge()).filter(v->v==1).orElse(3);
六、Date-Time API
这是对java.util.Date
强有力的补充,解决了 Date 类的大部分痛点:
- 非线程安全
- 时区处理麻烦
- 各种格式化、和时间计算繁琐
- 设计有缺陷,Date 类同时包含日期和时间;还有一个 java.sql.Date,容易混淆。
我们从常用的时间实例来对比 java.util.Date 和新 Date 有什么区别。用java.util.Date
的代码该改改了。
1. java.time 主要类
java.util.Date
既包含日期又包含时间,而 java.time
把它们进行了分离
LocalDateTime.class //日期+时间 format: yyyy-MM-ddTHH:mm:ss.SSS
LocalDate.class //日期 format: yyyy-MM-dd
LocalTime.class //时间 format: HH:mm:ss
2. 格式化
Java 8 之前:
public void oldFormat(){Date now = new Date();//format yyyy-MM-dd HH:mm:ssSimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");String date = sdf.format(now);System.out.println(String.format("date format : %s", date));//format HH:mm:ssSimpleDateFormat sdft = new SimpleDateFormat("HH:mm:ss");String time = sdft.format(now);System.out.println(String.format("time format : %s", time));//format yyyy-MM-dd HH:mm:ssSimpleDateFormat sdfdt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");String datetime = sdfdt.format(now);System.out.println(String.format("dateTime format : %s", datetime));
}
Java 8 之后:
public void newFormat(){//format yyyy-MM-ddLocalDate date = LocalDate.now();System.out.println(String.format("date format : %s", date));//format HH:mm:ssLocalTime time = LocalTime.now().withNano(0);System.out.println(String.format("time format : %s", time));//format yyyy-MM-dd HH:mm:ssLocalDateTime dateTime = LocalDateTime.now();DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");String dateTimeStr = dateTime.format(dateTimeFormatter);System.out.println(String.format("dateTime format : %s", dateTimeStr));
}
3. 字符串转日期格式
Java 8 之前:
//已弃用
Date date = new Date("2021-01-26");
//替换为
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date1 = sdf.parse("2021-01-26");
Java 8 之后:
LocalDate date = LocalDate.of(2021, 1, 26);
LocalDate.parse("2021-01-26");LocalDateTime dateTime = LocalDateTime.of(2021, 1, 26, 12, 12, 22);
LocalDateTime.parse("2021-01-26 12:12:22");LocalTime time = LocalTime.of(12, 12, 22);
LocalTime.parse("12:12:22");
Java 8 之前 转换都需要借助 SimpleDateFormat
类,而Java 8 之后只需要 LocalDate
、LocalTime
、LocalDateTime
的 of
或 parse
方法。
4. 日期计算
下面仅以一周后日期为例,其他单位(年、月、日、1/2 日、时等等)大同小异。另外,这些单位都在 java.time.temporal.ChronoUnit 枚举中定义。
Java 8 之前:
public void afterDay(){//一周后的日期SimpleDateFormat formatDate = new SimpleDateFormat("yyyy-MM-dd");Calendar ca = Calendar.getInstance();ca.add(Calendar.DATE, 7);Date d = ca.getTime();String after = formatDate.format(d);System.out.println("一周后日期:" + after);//算两个日期间隔多少天,计算间隔多少年,多少月方法类似String dates1 = "2021-12-23";String dates2 = "2021-02-26";SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");Date date1 = format.parse(dates1);Date date2 = format.parse(dates2);int day = (int) ((date1.getTime() - date2.getTime()) / (1000 * 3600 * 24));System.out.println(dates2 + "和" + dates2 + "相差" + day + "天");//结果:2021-12-23和2021-12-23相差300天
}
Java 8 之后:
public void pushWeek(){//一周后的日期LocalDate localDate = LocalDate.now();//方法1LocalDate after = localDate.plus(1, ChronoUnit.WEEKS);//方法2LocalDate after2 = localDate.plusWeeks(1);System.out.println("一周后日期:" + after);//算两个日期间隔多少天,计算间隔多少年,多少月LocalDate date1 = LocalDate.parse("2021-02-26");LocalDate date2 = LocalDate.parse("2021-12-23");Period period = Period.between(date1, date2);System.out.println("date1 到 date2 相隔:"+ period.getYears() + "年"+ period.getMonths() + "月"+ period.getDays() + "天");//打印结果是 “date1 到 date2 相隔:0年9月27天”//这里period.getDays()得到的天是抛去年月以外的天数,并不是总天数//如果要获取纯粹的总天数应该用下面的方法long day = date2.toEpochDay() - date1.toEpochDay();System.out.println(date2 + "和" + date2 + "相差" + day + "天");//打印结果:2021-12-23和2021-12-23相差300天
}
5. 获取指定日期
除了日期计算繁琐,获取特定一个日期也很麻烦,比如获取本月最后一天,第一天。
Java 8 之前:
public void getDay() {SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");//获取当前月第一天:Calendar c = Calendar.getInstance();c.set(Calendar.DAY_OF_MONTH, 1);String first = format.format(c.getTime());System.out.println("first day:" + first);//获取当前月最后一天Calendar ca = Calendar.getInstance();ca.set(Calendar.DAY_OF_MONTH, ca.getActualMaximum(Calendar.DAY_OF_MONTH));String last = format.format(ca.getTime());System.out.println("last day:" + last);//当年最后一天Calendar currCal = Calendar.getInstance();Calendar calendar = Calendar.getInstance();calendar.clear();calendar.set(Calendar.YEAR, currCal.get(Calendar.YEAR));calendar.roll(Calendar.DAY_OF_YEAR, -1);Date time = calendar.getTime();System.out.println("last day:" + format.format(time));
}
Java 8 之后:
public void getDayNew() {LocalDate today = LocalDate.now();//获取当前月第一天:LocalDate firstDayOfThisMonth = today.with(TemporalAdjusters.firstDayOfMonth());// 取本月最后一天LocalDate lastDayOfThisMonth = today.with(TemporalAdjusters.lastDayOfMonth());//取下一天:LocalDate nextDay = lastDayOfThisMonth.plusDays(1);//当年最后一天LocalDate lastday = today.with(TemporalAdjusters.lastDayOfYear());//2021年最后一个周日,如果用Calendar是不得烦死。LocalDate lastMondayOf2021 = LocalDate.parse("2021-12-31").with(TemporalAdjusters.lastInMonth(DayOfWeek.SUNDAY));
}
java.time.temporal.TemporalAdjusters
里面还有很多便捷的算法,这里就不带大家看 Api 了,都很简单,看了秒懂。
6. JDBC 和 java8
现在 jdbc 时间类型和 java8 时间类型对应关系是
Date
—>LocalDate
Time
—>LocalTime
Timestamp
—>LocalDateTime
而之前统统对应 Date
,也只有 Date
。
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
。
//当前时区时间
ZonedDateTime zonedDateTime = ZonedDateTime.now();
System.out.println("当前时区时间: " + zonedDateTime);//东京时间
ZoneId zoneId = ZoneId.of(ZoneId.SHORT_IDS.get("JST"));
ZonedDateTime tokyoTime = zonedDateTime.withZoneSameInstant(zoneId);
System.out.println("东京时间: " + tokyoTime);// ZonedDateTime 转 LocalDateTime
LocalDateTime localDateTime = tokyoTime.toLocalDateTime();
System.out.println("东京时间转当地时间: " + localDateTime);//LocalDateTime 转 ZonedDateTime
ZonedDateTime localZoned = localDateTime.atZone(ZoneId.systemDefault());
System.out.println("本地时区时间: " + localZoned);//打印结果
当前时区时间: 2021-01-27T14:43:58.735+08:00[Asia/Shanghai]
东京时间: 2021-01-27T15:43:58.735+09:00[Asia/Tokyo]
东京时间转当地时间: 2021-01-27T15:43:58.735
当地时区时间: 2021-01-27T15:53:35.618+08:00[Asia/Shanghai]
8. 小结
通过上面比较新老 Date
的不同,当然只列出部分功能上的区别,更多功能还得自己去挖掘。总之 date-time-api 给日期操作带来了福利。在日常工作中遇到 date 类型的操作,第一考虑的是 date-time-api,实在解决不了再考虑老的 Date。
Java基础学习——第十六章 Java8新特性相关推荐
- 夯实Java基础(二十二)——Java8新特性之Lambda表达式
1.前言 Java 8于14年发布到现在已经有5年时间了,经过时间的磨练,毫无疑问,Java 8是继Java 5(发布于2004年)之后的又一个非常最重要的版本.因为Java 8里面出现了非常多新的特 ...
- Java基础学习总结(55)——java8新特性:stream
java作为开发语言中的元老已经度过了很多年,最新的java8为我们带来了一些新特性,这些特性可以在以后的工作中为我们的开发提供更多的便捷,现在就让我们看看最新的函数式编程风格怎么在实际的开发中使用.
- Java基础学习——第十四章 网络编程
Java基础学习--第十四章 网络编程 一.网络编程概述 计算机网络: 把分布在不同地理区域的计算机与专门的外部设备用通信线路互连成一个规模大.功能强的网络系统,从而使众多的计算机可以方便地互相传递信 ...
- 零基础学习java------21---------动态代理,java8新特性(lambda, stream,DateApi)
1. 动态代理 在一个方法前后加内容,最简单直观的方法就是直接在代码上加内容(如数据库中的事务),但这样写不够灵活,并且代码可维护性差,所以就需要引入动态代理 1.1 静态代理实现 在讲动态代理之前, ...
- 【JAVA SE】第十六章 进程、线程、同步锁和线程锁的简介
第十六章 进程.线程.同步锁和线程安全问题 文章目录 第十六章 进程.线程.同步锁和线程安全问题 一.进程 1.基本介绍 2.进程模型 二.线程 1.基本介绍 2.线程的生命周期 3.线程的优先级 4 ...
- 鸟哥的Linux私房菜(基础篇)- 第十六章、例行性工作排程 (crontab)
第十六章.例行性工作排程 (crontab) 最近升级日期:2009/09/11 学习了基础篇也一阵子了,你会发现到为什么系统常常会主动的进行一些任务?这些任务到底是谁在配置工作的?如果你想要让自己设 ...
- 鸟哥的Linux私房菜(基础篇)- 第二十六章、Linux 核心编译与管理
第二十六章.Linux核心编译与管理 最近升级日期:2009/09/18 我们说的 Linux 其实指的就是核心 (kernel) 而已.这个核心控制你主机的所有硬件并提供系统所有的功能,所以说,他重 ...
- 【Linux命令】《鸟哥Linux基础》第十六章 进程管理与SELinux初探
第十六章 进程管理与SELinux初探 16.1 什么是进程(process) Linux下的所有命令与你能够执行的操作 ===>都与权限有关 如何判断权限? 账号管理中的UID.GID:文件属 ...
- Java基础学习总结(33)——Java8 十大新特性详解
Java8 十大新特性详解 本教程将Java8的新特新逐一列出,并将使用简单的代码示例来指导你如何使用默认接口方法,lambda表达式,方法引用以及多重Annotation,之后你将会学到最新的API ...
最新文章
- java 获取utc,如何在Java 8中获取UTC + 0日期?
- Linux纯脚本故障转移集群
- android 设颜色透明值
- 码位(code position/point)Unicode 编码与 Python 2/3 编码兼容性问题
- 静态程序分析chapter1 - 概述和两个重要步骤
- Linux下C编程入门(7)
- YBTOJ:前缀数组(KMP)
- CSS知识点整理(2):框模型,定位
- PHP函数之HTMLSPECIALCHARS_DECODE
- C++类的继承与派生
- 软件开发,维护与支持的困惑
- 如何动态为 tabstrip 中的 tab 页签指定标题
- clickhouse-小结 mutation操作 视图
- JavaScript lambda 表达式介绍
- JeeSite (三)前端
- 网页html通过隐藏域传送数据给web服务器
- 【论文翻译】ADVIO: An Authentic Dataset for Visual-Inertial Odometry
- 学会记录生活的每件小事
- cad引出线段lisp_给定起终点,如何提取线段连线关系表 - AutoLISP/Visual LISP 编程技术 - CAD论坛 - 明经CAD社区 - Powered by Discuz!...
- 强势杀入汽车B2B电商王者榜的卖好车,具备怎么样的核心竞争力
热门文章
- php怎么输出爱人的名字,【给爱人的暖心备注名称】
- XRD、TEM、AFM表征粒径
- kafka消费者 confluent_kafka
- 如何用PHP给上传的文件改名?
- 可以测试真假音的软件,想自由转换真假音,我教你啊!
- 工业4.0让德国制造业两年受损500亿?中国应怎样学习前车之鉴
- 2022年最新教程!Hexo + GitLab搭建个人博客详细教程
- Android 多点模拟触控 免root 非无障碍 思路
- 2023 年 最全的 24 个最佳免费照片恢复软件
- java 文件句柄_Java 文件句柄泄露问题解决小记(转)