java基础补充部分
点击查看基础部分: link
String部分
String、StringBuffer、StringBuilder 的区别?
- 可变性
- String 是不可变的
- StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用final 和 private 关键字修饰,最关键的是这个
AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
//...
}
-
线程安全性
- String 中的对象是不可变的,也就可以理解为常量,线程安全。
- AbstractStringBuilder 是StringBuilder 与 StringBuffer 的公共父类,定义了⼀些字符串的基本操作,如expandCapacity 、 append 、 insert 、 indexOf 等公共方法。 StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。 StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
-
性能
每次对 String 类型进⾏改变的时候,都会⽣成⼀个新的 String 对象,然后将指针指向新的String 对象。 StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者总结:
- 操作少量的数据: 适用 String
- 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer
String 为什么是不可变的?不能直接赋值改变?
String 类中使用 final 关键字修饰字符数组来保存字符串,所以 String 对象是不可变的。
private final char value[];
如果改变 String 的值,又不把它重新指向其他对象的话
利用反射修改
补充: 在Java 9之后, String 、 StringBuilder 与 StringBuffer 的实现改用 byte 数组存储字符串。
public final class String implements
java.io.Serializable,Comparable<String>, CharSequence {
// @Stable 注解表示变量最多被修改⼀次,称为“稳定的”。
@Stable
private final byte[] value;
}
abstract class AbstractStringBuilder implements Appendable, CharSequence {
byte[] value;
}
Java 9 为何要将 String 的底层实现由 char[] 改成了 byte[] ?
新版的 String 其实⽀持两个编码⽅案:
- Latin-1 和 UTF-16。如果字符串中包含的汉字没有超过 Latin-1 可表示范围内的字符,那就会使⽤ Latin-1作为编码⽅案。Latin-1 编码⽅案下, byte 占⼀个字节(8 位), char 占⽤ 2 个字节(16), byte 相j较char节省⼀半的内存空间。 JDK 官⽅就说了绝⼤部分字符串对象只包含 Latin-1 可表示的字符。
- 如果字符串中包含的汉字超过 Latin-1可表示范围内的字符, byte 和 char 所占用的空间是⼀ 样的。
官方介绍: link
字符串拼接用“+” 还是 StringBuilder?
Java 语眼本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符。
String str1 = "he";
String str2 = "llo";
String str3 = "world";
String str4 = str1 + str2 + str3;
- 字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder 调用append() 方法实现的,拼接完成之后调用toString() 得到⼀个 String 对象 。
- 不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个StringBuilder 以复用,会导致创建过多的 StringBuilder 对象。
- StringBuilder 对象是在循环内部被创建的,这意味着每循环⼀次就会创建⼀个 StringBuilder 对象。
- 如果直接使用 StringBuilder 对象进行字符串拼接的话,就不会存在这个问题了。
如果使用IDEA 的话,IDEA 自带的代码检查机制也会提示你修改代码。
String#equals() 和 Object#equals() 有何区别?
String 中的 equals 方法是被重写过的,比较的是 String 字符串的值是否相等。 Object 的equals 方法比较的是对象的内存地址。
字符串常量池的作用了解吗?
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的⼀块区域,
主要目的是为了避免字符串的重复创建。
// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引⽤保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引⽤
String bb = "ab";
System.out.println(aa==bb);// true
String s1 = new String(“abc”);这句话创建了个字符串对象?
会创建 1 或 2 个字符串对象。
1、如果字符串常量池中不存在字符串对象“abc”的引用,那么会在堆中创建 2 个字符串对象“abc”。
对应的字节码:
ldc 命令⽤于判断字符串常量池中是否保存了对应的字符串对象的引⽤,如果保存了的话直接返
回,如果没有保存的话,会在堆中创建对应的字符串对象并将该字符串对象的引⽤保存到字符串常量
池中。
2、如果字符串常量池中已存在字符串对象“abc”的引⽤,则只会在堆中创建 1 个字符串对象“abc”。
intern 方法有什么作用?
String.intern() 是⼀个 native(本地)方法,其作用是将指定的字符串对象的引⽤保存在字符串常量池中,可以简单分为两种情况:
- 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
- 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建⼀个指向该字符
串对象的引用并返回。
示例代码 :
// 在堆中创建字符串对象”Java“
// 将字符串对象”Java“的引⽤保存在字符串常量池中
String s1 = "Java";
// 直接返回字符串常量池中字符串对象”Java“对应的引⽤
String s2 = s1.intern();
// 会在堆中在单独创建⼀个字符串对象
String s3 = new String("Java");
// 直接返回字符串常量池中字符串对象”Java“对应的引⽤
String s4 = s3.intern();
// s1 和 s2 指向的是堆中的同⼀个对象
System.out.println(s1 == s2); // true
// s3 和 s4 指向的是堆中不同的对象
System.out.println(s3 == s4); // false
// s1 和 s4 指向的是堆中的同⼀个对象
System.out.println(s1 == s4); //true
String 类型的变量和常量做“+”运算时发⽣了什么?
先来看字符串不加 final 关键字拼接的情况
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";
String str4 = str1 + str2;
String str5 = "string";
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
注意 :比较String 字符串的值是否相等,可以使用equals() 方法。 String 中的 equals 方法是被重写过的。
Object 的 equals 方法是比较的对象的内存地址,而 String 的 equals方法比较的是字符串的值是否相等。如果你使⽤ =
= 比较两个字符串是否相等的话,IDEA 还是提示你使用 equals() 方法替换。
对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存⼊字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之⼀(代码优化⼏乎都在即时编译器中进行)。
对于 String str3 = “str” + “ing”; 编译器会给你优化成 String str3 = “string”; 。
并不是所有的常量都会进⾏折叠,只有编译器在程序编译期就可以确定值的常量才可以:
- 基本数据类型( byte 、 boolean 、 short 、 char 、 int 、 float 、 long 、 double )以及字符串常量。
- final 修饰的基本数据类型和字符串变量
- 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )
引用的值在程序编译期是无法确定的,编译器无法对其进行优化。
对象引⽤和“+”的字符串拼接⽅式,实际上是通过 StringBuilder 调⽤ append() ⽅法实现的,拼接完成之后调⽤ toString() 得到⼀个 String 对象 。
String str4 = new StringBuilder().append(str1).append(str2).toString();
我们在平时写代码的时候,尽量避免多个字符串对象拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使⽤ StringBuilder 或者 StringBuffer 。
不过,字符串使⽤ final 关键字声明之后,可以让编译器当做常量来处理。
final String str1 = "str";
final String str2 = "ing";
// 下⾯两个表达式其实是等价的
String c = "str" + "ing";// 常量池中的对象
String d = str1 + str2; // 常量池中的对象
System.out.println(c == d);// true
被 final 关键字修改之后的 String 会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就相当于访问常量。
如果 ,编译器在运⾏时才能知道其确切值的话,就⽆法对其优化。
示例代码( str2 在运⾏时才能确定其值):
final String str1 = "str";
final String str2 = getStr();
String c = "str" + "ing";// 常量池中的对象
String d = str1 + str2; // 在堆上创建的新的对象
System.out.println(c == d);// false
public static String getStr() {
return "ing";
}
反射
何谓反射?
反射被称为框架的灵魂,赋予了我们在运行时分析类以及执行类中方法的能力。通过反射你可以获取任意⼀个类的所有属性和方法,你还可以调用这些方法和属性。
反射的优缺点?
有了分析操作类的能力的同时也增加了安全问题,比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。
try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new
File(“test.txt”)));
BufferedOutputStream bout = new BufferedOutputStream(new
FileOutputStream(new File(“out.txt”)))) {
int b;
while ((b = bin.read()) != -1) {
bout.write(b);
}
}
catch (IOException e) {
e.printStackTrace();
}
相关阅读:Java Reflection: Why is it so slow? 。
反射的应⽤场景?
我们大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。但是!这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。
这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。
//通过 JDK 实现动态代理的示例代码,其中就使用了反射类 Method 来调用指定的方法。
public class DebugInvocationHandler implements InvocationHandler {
/**
* 代理类中的真实对象
*/
private final Object target;
public DebugInvocationHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws
InvocationTargetException, IllegalAccessException {
System.out.println("before method " + method.getName());
Object result = method.invoke(target, args);
System.out.println("after method " + method.getName());
return result;
}
}
另外,像 Java 中的利器注解的实现也用到了反射
为什么使⽤ Spring 的时候 ,⼀个 @Component 注解就声明了⼀个类为 Spring Bean 呢?
为什么你通过⼀个@Value 注解就读取到配置⽂件中的值呢?究竟是怎么起作⽤的呢?
- 因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进⼀步的处理
什么是SPI
SPI 即 Service Provider Interface ,(服务提供者的接口):专门提供给服务提供者或者扩展框架功能的开发者去使用的⼀个接口
SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。
很多框架都使用了 Java 的 SPI 机制,如:Spring 框架、数据库加载驱动、日志接口、以及Dubbo 的扩展实现等等
SPI 和 API 有什么区别?
广义上来说它们都属于接口
⼀般模块之间都是通过通过接⼝进⾏通讯,那我们在服务调用方和服务实现方(也称服务提供者)之间引入⼀个“接口”。
当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。
当接口存在于调用房这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根绝这个规则对这个接口进行实现,从而提供服务。
指定好标准,其他人就按照标准交付(提供不同⽅案的实
现,但是给出来的结果是⼀样的)
SPI 的优缺点?
通过 SPI 机制能够大大地提⾼接口设计的灵活性,但是 SPI 机制也存在⼀些缺点,如:
- 需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低。
- 当多个 ServiceLoader 同时 load 时,会有并发问题。
序列化
什么是序列化?什么是反序列化?
如果我们需要持久化 Java 对象如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。
- 序列化: 将数据结构或对象转换成⼆进制字节流的过程
- 反序列化:将在序列化过程中所⽣成的⼆进制字节流转换成数据结构或者对象的过程
对于 Java 这种⾯向对象编程语⾔来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半⾯向对象的语⾔中,struct(结构体)定义的是数据结构类型,而class 对应的是对象类型。
综上:序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中
如果有些字段不想进行序列化怎么办?
对于不想进行序列化的变量,使用 transient 关键字修饰。
transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复,注意:
- transient 只能修饰变量,不能修饰类和方法。
- transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int类型,那么反序列后结果就是 0 。
- static 变量因为不属于任何对象(Object),所以⽆论有没有 transient 关键字修饰,均不会被序列化
泛型&通配符
什么是泛型?有什么作用?
Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。
早期的Java使用Object来代表任意类型的,但是向下转型要强转,程序不太安全。
Java泛型设计原则:在编译期防止将错误类型的对象放置到集合中,运行时期就不会出现ClassCastException异常
编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 ArrayL#ist<Persion> persons = new ArrayList<Persion>()
这行代码就指明了该 ArrayList 对象只能传入 Persion 对象,如果传入其他类型的对象就会报错。
ArrayList<E> extends AbstractList<E>
并且,原生 List 返回类型是 Object ,需要手动转换类型才能使用,使用泛型后编译器自动转换。
泛型的使用方式有哪几种?
泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。
1.泛型类:
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
如何实例化泛型类:
Generic<Integer> genericInteger = new Generic<Integer>(123456);
2.泛型接口 :
public interface Generator<T> {
public T method();
}
实现泛型接口,不指定类型:
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method() {
return null;
}
}
实现泛型接口,指定类型:
class GeneratorImpl<T> implements Generator<String>{
@Override
public String method() {
return "hello";
}
}
3.泛型方法 :
public static < E > void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
使用:
// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray( intArray );
printArray( stringArray );
项目中哪里用到了泛型?
- 自定义接口通用返回结果 CommonResult 通过参数 T 可根据具体的返回类型动态指定结果的数据类型
- 定义 Excel 处理类 ExcelUtil 用于动态指定 Excel 导出的数据类型
- 构建集合工具类(参考 Collections 中的 sort, binarySearch 方法)。
- …
什么是泛型擦除机制?为什么要擦除?
Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除 。
编译器会在编译期间会动态地将泛型 T 擦除为 Object 或将 T extends xxx 擦除为其限定类型 xxx 。
因此,泛型本质上其实还是编译器的行为,为了保证引入泛型机制但不创建新的类型,减少虚拟机的运行开销,编译器通过擦除将泛型类转化为一般类。
举个例子:
List<Integer> list = new ArrayList<>();
list.add(12);
//1.编译期间直接添加会报错
list.add("a");
Class<? extends List> clazz = list.getClass();
Method add = clazz.getDeclaredMethod("add", Object.class);
//2.运行期间通过反射添加,是可以的
add.invoke(list, "kl");
System.out.println(list)
再举一个 : 由于泛型擦除的问题,下面的方法重载会报错。
public void print(List<String> list) { }
public void print(List<Integer> list) { }
原因也很简单,泛型擦除之后,List 与 List 在编译以后都变成了 List 。
既然编译器要把泛型擦除,那为什么还要用泛型呢?用 Object 代替不行吗?
这个问题其实在变相考察泛型的作用:
- 使用泛型可在编译期间进行类型检测。
- 使用 Object 类型需要手动添加强制类型转换,降低代码可读性,提高出错概率。
- 泛型可以使用自限定类型如 T extends Comparable 。
什么是桥方法?
桥方法(Bridge Method) 用于继承泛型类时保证多态。
class Node<T> {
public T data;
public Node(T data) { this.data = data; }
public void setData(T data) {
System.out.println("Node.setData");
this.data = data;
}
}
class MyNode extends Node<Integer> {
public MyNode(Integer data) { super(data); }
// Node<T> 泛型擦除后为 setData(Object data),而子类 MyNode 中并没有重写该方法,所以编译器会加入该桥方法保证多态
public void setData(Object data) {
setData((Integer) data);
}
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
⚠️注意 :桥方法为编译器自动生成,非手写。
泛型有哪些限制?为什么?
泛型的限制一般是由泛型擦除机制导致的。擦除为 Object 后无法进行类型判断
- 只能声明不能实例化 T 类型变量。
- 泛型参数不能是基本类型。因为基本类型不是 Object 子类,应该用基本类型对应的引用类型代替。
- 不能实例化泛型参数的数组。擦除后为 Object 后无法进行类型判断。
- 不能实例化泛型数组。
- 泛型无法使用 Instance of 和 getClass() 进行类型判断。
- 不能实现两个不同泛型参数的同一接口,擦除后多个父类的桥方法将冲突
- 不能使用 static 修饰泛型变量
- …
以下代码是否能编译,为什么?
public final class Algorithm {
public static <T> T max(T x, T y) {
return x > y ? x : y;
}
}
无法编译,因为 x 和 y 都会被擦除为 Object
类型,Object
无法使用>
进行比较
public class Singleton<T> {
public static T getInstance() {
if (instance == null)
instance = new Singleton<T>();
return instance;
}
private static T instance = null;
}
无法编译,因为不能使用 static
修饰泛型 T
。
通配符
什么是通配符?有什么作用?
泛型类型是固定的,某些场景下使用起来不太灵活,于是,通配符就来了!通配符可以允许类型参数变化,用来解决泛型无法协变的问题。
举个例子:
// 限制类型为 Person 的子类
<? extends Person>
// 限制类型为 Manager 的父类
<? super Manager>
通配符 ?和常用的泛型 T 之间有什么区别?
- T 可以用于声明变量或常量而 ? 不行。
- T 一般用于声明泛型类或方法,通配符 ? 一般用于泛型方法的调用代码和形参。
- T 在编译期会被擦除为限定类型或 Object,通配符用于捕获具体类型。
什么是无界通配符?
无界通配符可以接收任何泛型类型数据,用于实现不依赖于具体类型参数的简单方法,可以捕获参数类型并交由泛型方法进行处理。
void testMethod(Person<?> p) {
// 泛型方法自行处理
}
List<?> 和 List 有区别吗? 当然有!
- List<?> list 表示 list 是持有某种特定类型的 List,但是不知道具体是哪种类型。因此,我们添加元素进去的时候会报错。
- List list 表示 list 是持有的元素的类型是 Object,因此可以添加任何类型的对象,只不过编译器会有警告信息。
List<?> list = new ArrayList<>();
list.add("sss");//报错
List list2 = new ArrayList<>();
list2.add("sss");//警告信息
什么是上边界通配符?什么是下边界通配符?
在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。
上边界通配符 extends 可以实现泛型的向上转型即传入的类型实参必须是指定类型的子类型。
举个例子:
// 限制必须是 Person 类的子类
<? extends Person>
类型边界可以设置多个,还可以对 T 类型进行限制。
<T extends T1 & T2>
<T extends XXX>
下边界通配符 super 与上边界通配符 extends刚好相反,它可以实现泛型的向下转型即传入的类型实参必须是指定类型的父类型。
举个例子:
// 限制必须是 Employee 类的父类
List<? super Employee>
? extends xxx
和? super xxx
有什么区别?
两者接收参数的范围不同。并且,使用 ? extends xxx 声明的泛型参数只能调用 get() 方法返回 xxx 类型,调用 set() 报错。使用 ? super xxx 声明的泛型参数只能调用 set() 方法接收 xxx 类型,调用 get() 报错。
T extends xxx
和? extends xxx
又有什么区别?
T extends xxx 用于定义泛型类和方法,擦除后为 xxx 类型, ? extends xxx 用于声明方法形参,接收 xxx 和其子类型。
Class<?>
和Class
的区别?
直接使用 Class 的话会有一个类型警告,使用 Class<?> 则没有,因为 Class 是一个泛型类,接收原生类型会产生警告
以下代码是否能编译,为什么?
class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }
class Node<T> { /* ... */ }
Node<Circle> nc = new Node<>();
Node<Shape> ns = nc;
不能,因为Node<Circle>
不是 Node<Shape>
的子类
class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }
class Node<T> { /* ... */ }
class ChildNode<T> extends Node<T>{
}
ChildNode<Circle> nc = new ChildNode<>();
Node<Circle> ns = nc;
可以编译,ChildNode<Circle>
是 Node<Circle>
的子类
public static void print(List<? extends Number> list) {
for (Number n : list)
System.out.print(n + " ");
System.out.println();
}
可以编译,List<? extends Number>
可以往外取元素,但是无法调用add()
添加元素。