目录
说说有几种序列化方式?(json对象序列化、java对象流序列化、protobuff序列化)
Java SPI机制 (提供给实现方开发者的接口,可以有各自的接口实现)
构造方法有哪些特点? 是否可被 override? 不能被重写,可以被重载
静态方法为什么不能调用非静态成员? (静态方法存在的时候(类加载时就分配空间初始化了),实例方法(调用构造函数才会创建)可能还不存在)
浮点数运算的精度丢失问题? (计算机是二进制,表示数字,宽度有限)
序列化
什么是序列化?什么是反序列化?
序列化(Serialization)是指将对象转换为字节流的过程,以便能够将该对象保存到文件、数据库,或者进行网络传输。
反序列化(Deserialization)就是将字节流转换回对象的过程,以便构建原始对象。
Serializable 接口有什么用?
这个接口只是一个标记,没有具体的作用,但是如果不实现这个接口,在有些序列化场景会报错,所以一般建议,创建的 JavaBean 类都实现 Serializable。
serialVersionUID 又有什么用?
serialVersionUID 就是起验证作用。
private static final long serialVersionUID = 1L;
我们经常会看到这样的代码,这个 ID 其实就是用来验证序列化的对象和反序列化对应的对象 ID 是否一致。
如果没有显示指定 serialVersionUID ,则编译器会根据类的相关信息自动生成一个,可以认为是一个指纹。
所以如果你没有定义一个 serialVersionUID, 结果序列化一个对象之后,在反序列化之前把对象的类的结构改了,比如增加了一个成员变量,则此时的反序列化会失败。
Java 序列化不包含静态变量吗?
是的,序列化机制只会保存对象的状态,而静态变量属于类的状态,不属于对象的状态
如果有些变量不想序列化,怎么办?
对于不想进行序列化的变量,使用transient
关键字修饰。
transient
关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient
修饰的变量值不会被持久化和恢复。
transient
只能修饰变量,不能修饰类和方法。
说说有几种序列化方式?(json对象序列化、java对象流序列化、protobuff序列化)
Java 序列化方式有很多,常见的有三种:
- Java 对象序列化:Java 原生序列化方法即通过 Java 原生流(InputStream 和 OutputStream 之间的转化)的方式进行转化,一般是对象输出流
ObjectOutputStream
和对象输入流ObjectInputStream
。 - Json序列化:这个可能是我们最常用的序列化方式,Json 序列化的选择很多,一般会使用 jackson 包,通过 ObjectMapper 类来进行一些操作,比如将对象转化为 byte 数组或者将 json 串转化为对象。
- ProtoBuff 序列化:ProtocolBuffer 是一种轻便高效的结构化数据存储格式,ProtoBuff 序列化对象可以很大程度上将其压缩,可以大大减少数据传输大小,提高系统性能。
泛型
Java 泛型
Java 泛型(generics)是 JDK 5 中引入的一个新特性,它允许程序员在类、接口和方法中定义类型参数,从而提高代码的可重用性和类型安全性。
泛型的主要目的是在编译时进行类型检查,避免运行时的类型错误,并减少代码中的类型转换以及强制类型转换。
编译完成会对类型进行擦除(泛型擦除),以便与java泛型的类型安全性和向下兼容性要求,保持一致
泛型的使用方式
泛型类、泛型方法、泛型接口
项目中哪里用到了泛型?
Java1.8新特性
JDK 1.8 都有哪些新特性?
JDK 1.8 新增了不少新的特性,如 Lambda 表达式、接口默认方法、Stream API、日期时间 API、Optional 类等。
- Java 8 允许在接口中添加默认方法和静态方法。
public interface MyInterface {
default void myDefaultMethod() {
System.out.println("My default method");
}
static void myStaticMethod() {
System.out.println("My static method");
}
}
Java 8为什么要在接口中增加default方法呢?(扩展方法时,不破坏已有的实现类)
Java 8中引入default方法主要是为了增强接口的功能和提供向后兼容的能力。通过default方法,接口可以有具体的实现,这样,当一个接口需要扩展新的方法时,不会破坏已有的实现类。这对于在不改变现有代码基础上扩展API非常有用。
- Lambda 表达式和函数式接口
Lambda 表达式描述了一个代码块(或者叫匿名方法),可以将其作为参数传递给构造方法或者普通方法以便后续执行。
- Stream 是对 Java 集合框架的增强,它提供了一种高效且易于使用的数据处理方式。
- Java 8 引入了一个全新的日期和时间 API,位于
java.time
包中。这个新的 API 纠正了旧版java.util.Date
类中的许多缺陷。 - Optional 类:用来解决空指针异常的问题。
Lambda 表达式了解多少?
Lambda 表达式主要用于提供一种简洁的方式来表示匿名方法,使 Java 具备了函数式编程的特性。
比如说我们可以使用 Lambda 表达式来简化线程的创建:
new Thread(() -> System.out.println("Hello World")).start();
Lambda 允许把函数作为一个方法的参数(函数作为参数传递到方法中),使用 Lambda 表达式使代码更加简洁。(函数式编程)
比如说我们可以配合 Stream 流进行数据过滤:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
其中 n -> n % 2 == 0
就是一个 Lambda 表达式。表示传入一个参数 n,返回 n % 2 == 0
的结果。
Java8 有哪些内置函数式接口?
JDK 1.8 API 包含了很多内置的函数式接口。其中就包括我们在老版本中经常见到的 Comparator 和 Runnable,Java 8 为他们都添加了 @FunctionalInterface 注解,以用来支持 Lambda 表达式。
除了这两个之外,还有 Callable、Predicate、Function、Supplier、Consumer 等等。
Optional 了解吗?
Optional
是用于防范NullPointerException
。
可以将 Optional
看做是包装对象(可能是 null
, 也有可能非 null
)的容器。当我们定义了 一个方法,这个方法返回的对象可能是空,也有可能非空的时候,我们就可以考虑用 Optional
来包装它,这也是在 Java 8 被推荐使用的做法。
Optional<String> optional = Optional.of("bam");
optional.isPresent(); // true
optional.get(); // "bam"
optional.orElse("fallback"); // "bam"
optional.ifPresent((s) -> System.out.println(s.charAt(0))); // "b"
Stream 流用过吗?
Stream
流,简单来说,使用 java.util.Stream
对一个包含一个或多个元素的集合做各种操作。这些操作可能是 中间操作 亦或是 终端操作。 终端操作会返回一个结果,而中间操作会返回一个 Stream
流。
Stream 流一般用于集合,我们对一个集合做几个常见操作:
- Filter 过滤
stringCollection.stream().filter((s) -> s.startsWith("a")).forEach(System.out::println);// "aaa2", "aaa1"
- Sorted 排序
stringCollection.stream().sorted().filter((s) -> s.startsWith("a")).forEach(System.out::println);// "aaa1", "aaa2"
- Map 转换
stringCollection.stream().map(String::toUpperCase).sorted((a, b) -> b.compareTo(a)).forEach(System.out::println);// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
- Match 匹配
// 验证 list 中 string 是否有以 a 开头的, 匹配到第一个,即返回 trueboolean anyStartsWithA =
stringCollection.stream().anyMatch((s) -> s.startsWith("a"));System.out.println(anyStartsWithA); // true// 验证 list 中 string 是否都是以 a 开头的boolean allStartsWithA =
stringCollection.stream().allMatch((s) -> s.startsWith("a"));System.out.println(allStartsWithA); // false// 验证 list 中 string 是否都不是以 z 开头的,boolean noneStartsWithZ =
stringCollection.stream().noneMatch((s) -> s.startsWith("z"));System.out.println(noneStartsWithZ); // true
- Count 计数
count
是一个终端操作,它能够统计 stream
流中的元素总数,返回值是 long
类型。
// 先对 list 中字符串开头为 b 进行过滤,让后统计数量long startsWithB =
stringCollection.stream().filter((s) -> s.startsWith("b")).count();System.out.println(startsWithB); // 3
- Reduce
Reduce
中文翻译为:减少、缩小。通过入参的 Function
,我们能够将 list
归约成一个值。它的返回类型是 Optional
类型。
Optional<String> reduced =
stringCollection.stream().sorted().reduce((s1, s2) -> s1 + "#" + s2);
reduced.ifPresent(System.out::println);// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"
其他
Java SPI机制 (提供给实现方开发者的接口,可以有各自的接口实现)
SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。
SPI将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。很多框架都使用了 Java的SPI机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等
SPI和API有什么区别
一般模块之间都是通过接口进行通讯,那我们在服务调用方和服务实现方也称服务提供者)之间引入一个“接口”。
当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API,这种接口和实现都是放在实现方的;
当接口存在于调用方这边时,就是 SPI,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。
SPI的优缺点
通过 SPI机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如:
- 需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。
- 当多个 ServiceLoader 同时 load 时,会有并发问题
动态代理的几种方式
两种,分别是JDK 动态代理机制和CGLIB 动态代理机制。
JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。
另外,CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。就二者的效率来说,大部分情况都是 JDK 动态代理更优秀
面向对象和面向过程的区别
两者的主要区别在于解决问题的方式不同:
面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题
面向对象会先抽象出对象,然后用对象执行方法的方式解决问题
另外,面向对象开发的程序一般更易维护、易复用、易扩展。
创建一个对象用什么运算符? 对象实体与对象引用有何不同?
new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)
一个对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球);
一个对象可以有 n 个引用指向它 (可以用 n 条绳子系住一个气球)。
对象的相等和引用相等的区别
- 对象的相等性是通过比较对象的内容或状态来确定的,常用方法是
equals()
。 - 对象的引用相等性是通过比较对象的引用地址来确定的,使用
==
操作符进行判断。
类的构造方法的作用是什么
构造方法是一种特殊的方法,主要作用是完成对象的初始化工作。
如果一个类没有声明构造方法,该程序能正确执行吗?
如果一个类没有声明构造方法,也可以执行!
因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。
如果我们自己添加了类的构造方法(无论是否有参),Java 就不会再添加默认的无参数的构造方法了,我们一直在不知不觉地使用构造方法,这也是为什么我们在创建对象的时候后面要加一个括号(因为要调用无参的构造方法)。 Singleton instance=new Singleton();
如果我们重载了有参的构造方法记得都要把无参的构造方法也写出来(无论是否用到),因为这可以帮助我们在创建对象的时候少踩坑。
构造方法有哪些特点? 是否可被 override? 不能被重写,可以被重载
构造方法特点如下
- 名字与类名相同 private Singleton()
- 没有返回值,但不能用 void 声明构造函数
- 生成类的对象时自动执行,无需调用 (无参构造函数)
构造方法不能被 override (重写)但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况
成员变量与局部变量的区别有哪些?
- 从语法形式上看:成员变量是属于类的,⽽局部变量是在⽅法中定义的变量或是⽅法的参数;成员变量可以被 public , private , static 等修饰符所修饰,⽽局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
- 从变量在内存中的存储⽅式来看:如果成员变量是使⽤ static 修饰的,那么这个成员变量是属于类的,如果没有使⽤ static 修饰,这个成员变量是属于实例的。对象存于堆内存。如果局部变量类型为基本数据类型,那么存储在栈内存,如果为引⽤数据类型,那存放的是指向堆内存对象的引⽤或者是指向常量池中的地址。
- 从变量在内存中的⽣存时间上看:成员变量是对象的⼀部分,它随着对象的创建⽽存在,⽽局部变量随着⽅法的调⽤⽽⾃动消失。
- 成员变量如果没有被赋初值:则会⾃动以类型的默认值⽽赋值(⼀种情况例外:被 final 修饰的成员变量也必须显式地赋值),⽽局部变量则不会⾃动赋值。
静态变量和实例变量的区别?静态方法、实例方法呢?
静态变量: 是被 static 修饰符修饰的变量,也称为类变量,它属于类,不属于类的任何一个对象,一个类不管创建多少个对象,静态变量在内存中有且仅有一个副本。
实例变量: 必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存。
静态方法:static 修饰的方法,也被称为类方法。在外部调⽤静态⽅法时,可以使⽤"类名.⽅法名"的⽅式,也可以使⽤"对象名.⽅法名"的⽅式。静态方法里不能访问类的非静态成员变量和方法。
实例⽅法:依存于类的实例,需要使用"对象名.⽅法名"的⽅式调用;可以访问类的所有成员变量和方法。
非静态内部类和静态内部类的区别?
区别包括:
- 非静态内部类依赖于外部类的实例,而静态内部类不依赖于外部类的实例。
- 非静态内部类可以访问外部类的实例变量和方法,而静态内部类只能访问外部类的静态成员。
- 非静态内部类不能定义静态成员,而静态内部类可以定义静态成员。
- 非静态内部类在外部类实例化后才能实例化,而静态内部类可以独立实例化。
- 非静态内部类可以访问外部类的私有成员,而静态内部类不能直接访问外部类的私有成员,需要通过实例化外部类来访问。
非静态内部类可以直接访问外部方法,编译器是怎么做到的?
非静态内部类可以直接访问外部方法是因为编译器在生成字节码时会为非静态内部类维护一个指向外部类实例的引用。
这个引用使得非静态内部类能够访问外部类的实例变量和方法。编译器会在生成非静态内部类的构造方法时,将外部类实例作为参数传入,并在内部类的实例化过程中建立外部类实例与内部类实例之间的联系,从而实现直接访问外部方法的功能。
Java和C++区别
Java 和 C++都是面向对象的语言,都支持封装、继承和多态,但是,它们还是有挺多不相同的地方:
- Java 不提供指针来直接访问内存,程序内存更加安全
- Java的类是单继承的,C++支持多重继承; 虽然Java的类不可以多继承,但是接口可以多继承。
- Java有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。
- C ++同时支持方法重载和操作符重载,但是Java 只支持方法重载(操作符重载增加了复杂性,这与Java 最初的设计思想不符)
静态方法为什么不能调用非静态成员? (静态方法存在的时候(类加载时就分配空间初始化了),实例方法(调用构造函数才会创建)可能还不存在)
静态方法和实例方法有什么不同?
浮点数运算的精度丢失问题? (计算机是二进制,表示数字,宽度有限)
超过long整型的数据应该如何表示? BigInteger
BigInteger
自己整理,借鉴很多博主 感谢他们