java 八股个人总结
java 八股个人总结
1.0 构造函数问题
对于一个类,如果没有构造函数,会自动创建一个无参构造函数并自动调用父类的无参构造函数
如果我们显示定义了构造函数,就不会生成这个构造函数
在执行子类构造函数之前,必须保证父类被初始化,所以我们自定义的构造函数都会在第一行插入调用父类无参构
造函数的代码
如果父类没有无参构造函数,这种插入就无法完成,我们必须手动去调用父类的有参构造函数
1.1 重写重载问题
重载要求在同一个类中,方法名必须相同,参数必须与原方法有区别,访问修饰符和返回类型不做要求,对于重载
方法来说,是在编译时进行静态绑定
重载要求存在类继承关系,方法名,参数列表(参数类型,参数顺序,参数个数,参数名不做要求,但最好一致),
必须和被重写的函数相同,返回类型必须与父类方法返回类型相同或者是其子类,如果方法的返回类型是 void 和
基本数据类型,则返回值重写时不可修改。但是如果方法的返回值是引用类型,重写时是可以返回该引用类型的子
类的,子类重写的方法访问修饰符权限不能低于父类的方法,重写方法在运行时绑定
对于重载方法参数中包含可变长参数时,会优先匹配不包含可变长参数的方法
对于构造方法来说,不能被重写(override),但可以被重载(overload)。
1.2 常量池和缓存问题
对于包装类,也都在一定的范围内也都存在缓存
Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创
建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 TRUE or FALSE。
字符串字面量会被存入常量池
String s1 ="hello"
此时jvm会现在常量池中进行搜索,如果找到了会取出找到的对象,将引用传过去,如果没找到,会创建新对象,
并将hello 放入常量池 ,如果此时有第二个变量String s2="hello" 此时从常量池取出的是同一个对象
所以s1=s2 返回结果是正确的,常量位于栈中
但如果使用new 的方式创建字符串对象,比如String s1=new String("hello")  此时会创建一个新的对象去存
储,对象存放在堆中
需要注意的是,只要在代码中出现字符串字面量,就会将其放进常量池,也就是对
String s1=new String("hello")
因为有"hello" 所以此时会检测常量池中是否有该常量,如果没有就创建放入,然后再是创建一个新的字符串对
象
1.3 引用地址
对于引用数据类型来说,使用==比较运算符比较的是对象,也可也说是引用的值,因为当对象相同时,对应的地
址相同,引用中的值也相同 ,想要比较实例的值,需要使用equals()  equals() 不能用于判断基本数据类型的变
量,只能用来判断两个对象是否相等
那引用变量的地址又放在哪里呢,对于局部变量来说,存放在java虚拟机栈中,每个线程私有,随方法调用入栈
对于成员变量来说,随对象一起存放在堆中
对于静态变量,存放在方法区,也就是本地内存空间中
Object 中定义了equals方法,但定义的方法调用的是== 进行比较,如果想要实现对对象的比较,不能直接使用
,需要在子类中进行重写,包装类中都对此方法进行了重写,可以实现对对象的比较,对象比较的依据是对字段进
行一一比较,如果两个对象字段的值都相等, 那就比较成功
1.4 抽象类和接口
成员变量:接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值。抽象类的成
员变量可以有任何修饰符(private, protected, public),可以在子类中被重新定义或赋值。
1.3 深拷贝和浅拷贝
浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型
的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
Object 提供的clone方法是浅拷贝方法,所以我们使用clone进行对象拷贝时 ,返回的值中如果有引用数据类型,
新对象和旧对象的值是相同的。
1.4 hashCode()方法
java中的Object类中有默认的hashCode方法,该方法基于引用的值进行哈希,用来比较两个对象是否相同肯定是
不行的,所以很多类会去重写hashCode方法,将字段加入hashCode过程
如果两个对象的hashCode 值相等,那这两个对象不一定相等(因为存在哈希碰撞)。
如果两个对象的hashCode 值相等并且equals()方法也返回 true,我们才认为这两个对象相等。
如果两个对象的hashCode 值不相等,我们就可以直接认为这两个对象不相等
对于用到HashCode的数据结构,比如HashMap 和HashSet 我们必须对其键的hashCode 和equals方法进行重写
现在分情况进行讨论:
1 只重写hashCode 没有重写equals :
hashMap 要求将对象作为键时,对于值相同的对象应该看作同一个键
hansMap中插入键的位置是通过hashcode进行判断的,此时对于两个值相同的对象,因为重写了hashCode 所以
在判断插入桶的位置时,两个对象得到的结果是相同的,接下来使用equals进行比较,如果相同,则对上一个对
象插入的结果进行覆盖,如果不同,则在我们hashCode得到的桶位置的链表或者红黑树的末尾
所以结果是覆盖失败
2 只重写equals 没有重写 hashCode
在一开始的判断过程就将其当作不同的键处理了,两个值插入到了不同的地方
在重写equals方法时必须重写hashCode方法
1.5 StringBuilder和StringBuffer
这两个类用于解决String 的不可变性质
- 操作少量的数据: 适用 
String - 单线程操作字符串缓冲区下操作大量数据: 适用 
StringBuilder - 多线程操作字符串缓冲区下操作大量数据: 适用 
StringBuffer 
1.6 泛型和通配符
Java中的泛型分为泛型类,泛型接口,泛型方法,其中泛型类和泛型接口都是需要主动指定泛型类型
泛型方法是通过传入的参数类型自己进行判断的
需要注意的是,泛型类中如果声明了静态泛型方法,此时方法中是无法使用泛型类中的泛型的,因为泛型类中的方
法是在实例创建时才确定的,如果在静态方法中使用,静态方法通过类调用的时候没有实例,无法确定泛型
使用泛型类调用静态方法的方式
类名.方法
泛型只在编译期间存在,编译成字节码后,所有的泛型都会被擦除,变成Object类型,
举个例子说明:
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)
再来举一个例子 : 由于泛型擦除的问题,下面的方法同时重载会报错。
原因也很简单,泛型擦除之后,List<String> 与 List<Integer> 在编译以后都变成了 List 。
public void print(List<String> list)  { }
public void print(List<Integer> 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
如果是无界通配符,在使用是是只读的,像
List<?> list = new ArrayList<>(); list.add("sss");//报错上界通配符(
? extends T)—— 只读 + 类型更明确public double sum(List<? extends Number> numbers) { double total = 0; for (Number n : numbers) { total += n.doubleValue(); } return total; } sum(Arrays.asList(1, 2.5, 3L)); // ✅ Integer, Double, Long 都是 Number 子类✅ 你可以安全地读取为
Number,但仍然不能 add(因为可能是List<Integer>,不能加Double)。下界通配符(
? super T)—— 可写(用于“消费者”)public void addNumbers(List<? super Integer> list) { list.add(1); // ✅ 允许!因为 list 至少能接受 Integer list.add(2); // 但读取时只能得到 Object Object obj = list.get(0); }当前此时使用泛型也可也这样写
通配符和泛型的区别如下:
通配符不能用于“类型关联”
场景:从一个 list 复制到另一个 list
// ❌ 编译错误! public static void copy(List<?> src, List<?> dest) { for (Object o : src) { dest.add(o); // Error: Object 不能添加到 List<?> } }原因:编译器不知道
src和dest的?是不是同一个类型。正确做法:用
<T>public static <T> void copy(List<T> src, List<T> dest) { for (T item : src) { dest.add(item); // ✅ 类型一致,安全 } }通配符不能作为返回类型(有意义的)
// 虽然能编译,但返回 Object,失去类型信息 public static Object getFirst(List<?> list) { return list.get(0); } // ✅ 用 <T> 可以保留类型 public static <T> T getFirst(List<T> list) { return list.get(0); } // 使用: String s = getFirst(stringList); // ✅ 类型安全通配符不能用于创建实例或调用泛型方法
public static void process(List<?> list) { // ❌ 无法知道 ? 是什么类型,不能 new // ? item = new ?(); // 语法错误! // ❌ 无法调用需要具体类型的泛型方法 // doSomething(item); // 如果 doSomething 要求 T,这里不行 }总而言之,我们使用泛型得到的类型在传入时都是固定的,确定了后类型就固定了,而通配符是一直不固定
的,二者的区别就在这里 ,还有一个区别是
配符(
?)不能作为类型变量名使用也就是不能像 T a 这样使用 ? a ,也就是说只能出现在泛型容器中,比如
List<T> list这样的通配符 vs 泛型方法:如何选择?
需求 推荐写法 只读数据,不关心具体类型(如 sum,print,contains)List<? extends T>(通配符)✅需要返回与输入相同的具体类型 <T extends T> T method(List<T>)(泛型方法)✅方法内部需要多次使用同一个具体类型 泛型方法 API 简洁性优先,且无类型关联需求 通配符  
1.7 序列化和反序列化
static修饰的变量正常是不会被序列化的,存储在方法区,但是我们定义的
private static final long serialVersionUID =设置的值
是会被序列化的,因为这是java中判断序列化对象是否可被反序列化的依据,java会比较当前被序列化的对象中的
字段和需要反序列化的对象中的该字段是否一致,如果不一致就抛出异常,使用该机制确保双方对象版本一致,
如果我们不去定义,会自动根据当前类的字段来生成
需要注意的是,当我们自定义该字段,是否抛出异常会根据下面情况判断
serialVersionUID 不一致 | 
✅ 是 | InvalidClassException | 
|---|---|---|
| 新增字段 | ❌ 否 | 新字段用默认值 | 
| 删除字段 | ❌ 否 | 旧数据被忽略 | 
| 字段重命名 | ❌ 否 | 视为删除+新增,数据丢失 | 
| 字段类型改变(不兼容) | ✅ 是 | InvalidClassException(字段签名不匹配) | 
| 修改访问修饰符 | ❌ 否 | 无影响 | 
1.8 代理
灵活性:动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
JVM 层面:静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
对于JDK动态代理,被代理的类必须继承有任意一个接口,代理对象拦截的就是这个接口中定义的类
对于CGLIB动态代理,不需要被代理的类继承有接口了,默认会拦截所有方法,不过可以指定需要被代理的方法
1.9 BigDecimal
对于浮点数来说,由于计算机内部保存浮点数的方法原因,我们实际在进行浮点数计算时会存在精度丢失,并且
无论是基本数据类型还是包装类,都无法使用==或者.equals来进行比较
这是因为 equals() 方法不仅仅会比较值的大小(value)还会比较精度 对于1.0和1用equals结果是false
这对于一些要求高的业务来说肯定是不行的,所以这时候就需要使用到BigDecimal
我们在使用 BigDecimal 时,为了防止精度丢失,推荐使用它的BigDecimal(String val)构造方法或者
BigDecimal.valueOf(double val) 静态方法来创建对象

                
            
浙公网安备 33010602011771号