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<?>
        }
    }
    

    原因:编译器不知道 srcdest? 是不是同一个类型。

    正确做法:用 <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) 静态方法来创建对象

posted @ 2025-10-31 14:51  折翼的小鸟先生  阅读(2)  评论(0)    收藏  举报