[TOC]
0x00 JDK8 引入的新特性 && 学习
1.接口的默认方法和静态方法(接口增强)
在Java8之前,接口中只能包含抽象方法。那么这有什么样弊端呢?比如,想再Collection接口中添加一个spliterator抽象方法,那么也就意味着之前所有实现Collection接口的实现类,都要重新实现spliterator这个方法才行。而接口的默认方法就是为了解决接口的修改与接口实现类不兼容的问题,作为代码向前兼容的一个方法。
那么如何在接口中定义一个默认方法呢?来看下JDK中Collection中如何定义spliterator方法的:
1 | default Spliterator<E> spliterator() { |
可以看到定义接口的默认方法是通过default关键字。因此,在Java8中接口能够包含抽象方法外还能够包含若干个默认方法(即有完整逻辑的实例方法)。
1 | public interface IAnimal { |
注意⚠️:因为class可以继承多个Interface,所以如果一个class集成了多个含有相同名称的default方法的接口,就会报错,这个时候我们需要通过override来重写它。
1
2
3
4
5
6
7
8
9
10
11
12 > public class DefaultMethodTest implements IAnimal,IDonkey,IHorse {
> public static void main(String[] args) {
> DefaultMethodTest defaultMethod = new DefaultMethodTest();
> defaultMethod.run();
> }
>
>
> public void run() {
> IHorse.super.run();
> }
> }
>
静态方法
在Java8中还有一个特性就是,接口中还可以声明静态方法并且可以实现,如下例:
1 | public interface IAnimal { |
2.函数式接口FunctionInterface与lambda表达式
函数式接口
Java8最大的变化是引入了函数式思想,也就是说函数可以作为另一个函数的参数。函数式接口,要求接口中有且仅有一个抽象方法,因此经常使用的Runnable,Callable接口就是典型的函数式接口。可以使用@FunctionalInterface
注解,声明一个接口是函数式接口。如果一个接口满足函数式接口的定义,会默认转换成函数式接口。但是,最好是使用@FunctionalInterface
注解显式声明。这是因为函数式接口比较脆弱,如果开发人员无意间新增了其他方法,就破坏了函数式接口的要求,如果使用注解@FunctionalInterface
,开发人员就会知道当前接口是函数式接口,就不会无意间破坏该接口。下面举一个例子:
1 | .lang.FunctionalInterface |
该接口只有一个抽象方法,并且使用注解显式声明。但是,函数式接口要求只有一个抽象方法却可以拥有若干个默认方法的(实例方法),比如下例:
1 |
|
该接口中,除了有抽象方法handle外,还有默认方法(实例方法)run。另外,任何被Object实现的方法都不能当做是抽象方法。
lambda表达式
lambda表达式是函数式编程的核心,lambda表达式即匿名函数,是一段没有函数名的函数体,可以作为参数直接传递给相关的调用者。lambda表达式极大的增加了Java语言的表达能力。lambda的语法结构为:
1 | (parameters) -> expression |
- 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
- 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
- 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
- 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。
完整示例为(摘自菜鸟)
1 | public class Java8Tester { |
另外,lambda还可以访问外部局部变量,如下例所示:
1 | int adder = 5; |
实际上在lambda中访问类的成员变量或者局部变量时,会隐式转换成final类型变量,所以上例实际上等价于:
1 | final int adder = 5; |
3. 方法引用
方法引用是为了进一步简化lambda表达式,通过类名或者实例名与方法名的组合来直接访问到类或者实例已经存在的方法或者构造方法。方法引用使用::来定义,::的前半部分表示类名或者实例名,后半部分表示方法名,如果是构造方法就使用NEW
来表示。
方法引用在Java8中使用方式相当灵活,总的来说,一共有以下几种形式:
- 静态方法引用:ClassName::methodName;
- 实例上的实例方法引用:instanceName::methodName;
- 超类上的实例方法引用:supper::methodName;
- 类的实例方法引用:ClassName:methodName;
- 构造方法引用Class:new;
- 数组构造方法引用::TypeName[]::new
下面来看一个例子:
1 | public class MethodReferenceTest { |
在上面的例子中使用了Car::new
,即通过构造方法的方法引用的方式进一步简化了lambda的表达式,Car::showCar
,即表示实例方法引用。
4.Stream
Java8中有一种新的数据处理方式,那就是流Stream,结合lambda表达式能够更加简洁高效的处理数据。Stream使用一种类似于SQL语句从数据库查询数据的直观方式,对数据进行如筛选、排序以及聚合等多种操作。
4.1 生成stream的方式
生成Stream的方式主要有这样几种:
- 从接口Collection中和Arrays:
- Collection.stream();
- Collection.parallelStream(); //相较于串行流,并行流能够大大提升执行效率
- Arrays.stream(T array);
- Stream中的静态方法:
- Stream.of();
- generate(Supplier s);
- iterate(T seed, UnaryOperator f);
- empty();
- 其他方法
- Random.ints()
- BitSet.stream()
- Pattern.splitAsStream(java.lang.CharSequence)
- JarFile.stream()
- BufferedReader.lines()
1 | public class StreamTest { |
4.2 stream的操作
过滤元素为空的字符串:
1 | long count = stream.filter(str -> str.isEmpty()).count(); |
map:对Stream中元素按照指定规则映射成另一个元素
将每一个元素都添加字符串“_map”
1 | stream.map(str -> str + "_map").forEach(System.out::println); |
map方法是一对一的关系,将stream中的每一个元素按照映射规则成另外一个元素,而如果是一对多的关系的话就需要使用flatmap方法。
concat:对流进行合并操作
concat方法将两个Stream连接在一起,合成一个Stream。若两个输入的Stream都时排序的,则新Stream也是排序的;若输入的Stream中任何一个是并行的,则新的Stream也是并行的;若关闭新的Stream时,原两个输入的Stream都将执行关闭处理。
1 | Stream.concat(Stream.of(1, 2, 3), Stream.of(4, 5, 6)). |
distinct:对流进行去重操作
去除流中重复的元素
1 | Stream<String> stream = Stream.of("a", "a", "b", "c"); |
limit:限制流中元素的个数
截取流中前两个元素:
1 | Stream<String> stream = Stream.of("a", "a", "b", "c"); |
skip:跳过流中前几个元素
丢掉流中前两个元素:
1 | Stream<String> stream = Stream.of("a", "a", "b", "c"); |
peek:对流中每一个元素依次进行操作,类似于forEach操作
JDK中给出的例子:
1 | Stream.of("one", "two", "three", "four") |
sorted:对流中元素进行排序,可以通过sorted(Comparator<? super T> comparator)自定义比较规则
1 | Stream<Integer> stream = Stream.of(3, 2, 1); |
match:检查流中元素是否匹配指定的匹配规则
Stream 有三个 match 方法,从语义上说:
- allMatch:Stream 中全部元素符合传入的 predicate,返回 true;
- anyMatch:Stream 中只要有一个元素符合传入的 predicate,返回 true;
- noneMatch:Stream 中没有一个元素符合传入的 predicate,返回 true。
如检查Stream中每个元素是否都大于5:
1 | Stream<Integer> stream = Stream.of(3, 2, 1); |
结束操作
count:统计Stream中元素的个数
1 | long count = stream.filter(str -> str.isEmpty()).count(); |
max/min:找出流中最大或者最小的元素
1 | Stream<Integer> stream = Stream.of(3, 2, 1); |
forEach
forEach方法前面已经用了好多次,其用于遍历Stream中的所元素,避免了使用for循环,让代码更简洁,逻辑更清晰。
示例:
1 | Stream.of(5, 4, 3, 2, 1) |
1
2
3 > T reduce(T identity, BinaryOperator<T> accumulator)
>
>
>
reduce 的第一个参数t为上次函数计算的返回值,第二个参数u为Stream中的元素
示例:
1 | int value = Stream.of(1, 2, 3, 4).reduce(100, (sum, item) -> sum + item); |
5 Optional
为了解决空指针异常,在Java8之前需要使用if-else这样的语句去防止空指针异常,而在Java8就可以使用Optional来解决。Optional可以理解成一个数据容器,甚至可以封装null,并且如果值存在调用isPresent()方法会返回true。
1 | public class OptionalTest { |
事实上,getUserName方法对输入参数并没有进行判断是否为null,因此,该方法是不安全的。如果在Java8之前,要避免可能存在的空指针异常的话就需要使用if-else
进行逻辑处理,getUserName会改变如下:
1 | private String getUserName(User user) { |
这是十分繁琐的一段代码。而如果使用Optional则会要精简很多:
1 | private String getUserName(User user) { |
6 Date/Time API改进
在Java8之前的版本中,日期时间API存在很多的问题,比如:
- 线程安全问题:java.util.Date是非线程安全的,所有的日期类都是可变的;
- 设计很差:在java.util和java.sql的包中都有日期类,此外,用于格式化和解析的类在java.text包中也有定义。而每个包将其合并在一起,也是不合理的;
- 时区处理麻烦:日期类不提供国际化,没有时区支持,因此Java中引入了java.util.Calendar和Java.util.TimeZone类;
针对这些问题,Java8重新设计了日期时间相关的API,Java 8通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理。在java.util.time包中常用的几个类有:
- 它通过指定一个时区,然后就可以获取到当前的时刻,日期与时间。Clock可以替换System.currentTimeMillis()与TimeZone.getDefault()
- Instant:一个instant对象表示时间轴上的一个时间点,Instant.now()方法会返回当前的瞬时点(格林威治时间);
- Duration:用于表示两个瞬时点相差的时间量;
- LocalDate:一个带有年份,月份和天数的日期,可以使用静态方法now或者of方法进行创建;
- LocalTime:表示一天中的某个时间,同样可以使用now和of进行创建;
- LocalDateTime:兼有日期和时间;
- ZonedDateTime:通过设置时间的id来创建一个带时区的时间;
- DateTimeFormatter:日期格式化类,提供了多种预定义的标准格式;
示例代码如下:
1 | public class TimeTest { |
0x02 什么是字节码 && 采用字节码的最大好处
Java 中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。
编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在 Java 中,这种供虚拟机理解的代码叫做字节码(即扩展名为 .class
的文件),它不面向任何特定的处理器,只面向虚拟机。
每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java 源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。这也就是解释了 Java 的编译与解释并存的特点。
1 | Java 源代码 |
🦅 采用字节码的好处?
Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。
0x03 Java 基本数据类型 && 相关的问题
基本数据类型如下:
- 整数值型:
byte
、short
、int
、long
- 字符型:
char
- 浮点类型:
float
、double
- 布尔型:
boolean
- 整数型:默认
int
型,小数默认是double
型。Float 和 Long 类型的必须加后缀。比如:float f = 100f
。
引用类型声明的变量是指该变量在内存中实际存储的是一个引用地址,实体在堆中。
- 引用类型包括类、接口、数组等。
- 特别注意,String 是引用类型不是基本类型。
🦅 什么是值传递和引用传递?
- 值传递,是对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量。
- 引用传递,一般是对于对象型变量而言的,传递的是该对象地址的一个副本,并不是原对象本身。
一般认为,Java 内的传递都是值传递,Java 中实例对象的传递是引用传递。
🦅 是否可以在 static 环境中访问非 static 变量?
static
变量在 Java 中是属于类的,它在所有的实例中的值是一样的。当类被 Java 虚拟机载入的时候,会对 static
变量进行初始化。
如果你的代码尝试不用实例来访问非 static
的变量,编译器会报错,因为这些变量还没有被创建出来,还没有跟任何实例关联上。
🦅 char 型变量中能不能存贮一个中文汉字?为什么?
- 在 C 语言中,char 类型占 1 个字节,而汉字占 2 个字节,所以不能存储。
- 在 Java 语言中,char 类型占 2 个字节,而且 Java 默认采用 Unicode 编码,一个 Unicode 码是 16 位,所以一个 Unicode 码占两个字节,Java 中无论汉字还是英文字母,都是用 Unicode 编码来表示的。所以,在 Java 中,char 类型变量可以存储一个中文汉字。
0x04 String、StringBuffer、StringBuilder 的区别?
Java 平台提供了两种类型的字符串:String 和 StringBuffer/StringBuilder,它们可以储存和操作字符串。
String ,是只读字符串,也就意味着 String 引用的字符串内容是不能被改变的。
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。
StringBuffer/StringBuilder 类,表示的字符串对象可以直接进行修改。StringBuilder 是 Java 5 中引入的,它和 StringBuffer 的方法完全相同,区别在于它是在单线程环境下使用的,因为它的所有方面都没有被
synchronized
修饰,因此它的效率也比 StringBuffer 要高。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。
相同情况下使用 StirngBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
🦅 对于三者使用的总结?
操作少量的数据 = String 。
这个也是实际编码较为经常使用的方式。
单线程操作字符串缓冲区下操作大量数据 = StringBuilder 。
甚至有时,我们为了避免每个线程重复创建 StringBuilder 对象,会通过 ThreadLocal + StringBuilder 的方式,进行对 StringBuilder 的重用。具体可以参考 《StringBuilder 在高性能场景下的正确用法》 文章。
多线程操作字符串缓冲区下操作大量数据 = StringBuffer
实际场景下,我们基本不太会出现,多线程操作同一个 StringBuffer 对象。
⚠️ String s = new String(“xyz”) 会创建几个对象?
- 首先,在 String 池内找,找到
"xyz"
字符串,不创建"xyz"
对应的 String 对象,否则创建一个对象。 - 然后,遇到
new
关键字,在内存上创建 String 对象,并将其返回给s
,又一个对象。
所以,总共是 1 个或者 2 个对象。
具体的,可以看看 《关于String s = new String(“xyz”); 创建几个对象的问题》 文章的测试代码。
🦅 String 为什么是不可变的?
简单的来说,String 类中使用 final
关键字字符数组保存字符串。代码如下:
1 | // String.java |
- 所以 String 对象是不可变的。
而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串 char[] value
,但是没有用 final
关键字修饰。代码如下:
1 | // AbstractStringBuilder.java |
- 所以这两种对象都是可变的。
🦅 StringTokenizer 是什么?
StringTokenizer ,是一个用来分割字符串的工具类。
示例代码如下:
1 | StringTokenizer st = new StringTokenizer(”Hello World”); |
输出如下:
1 | Hello |
参考
精尽Java基础面试题
- 本文作者: Noisy
- 本文链接: http://Metatronxl.github.io/2019/11/29/Java-基础知识储备-一/
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!