Skip to content

JavaSE

1. 八种基本数据类型注意

  • java八种基本数据类型的字节数:1字节(byte、boolean)、2字节(short、char)、4字节(int、float)、8字节(long、double)

  • 浮点数的默认类型为double(如果需要声明一个常量为float型,则必须要在未尾加上f或F)

  • 整数的默认类型为int(声明Long型在末尾加上l或者L)

  • 八种基本数据类型的包装类:除了char的是Character、int类型的是Integer,其他都是首字母大写

  • char类型是无符号的,不能为负,所以是0开始的

2. 为什么用decimal而不用double

decimal 是精确计算,一般牵涉到金钱的计算,都使用Decimal

java
BigDecimal num1 = new BigDecimal("0.1");
BigDecimal num2 = new BigDecimal("0.2");
BigDecimal sum = num1.add(num2);
BigDecimalproduct =num1.multiply(num2);
System.out.println("sum:+ sum");
System.out.println("Product:product");

double会出现精度丢失的问题,double执行的是 二进制浮点运算 ,二进制有些情况下不能准确的表示一小数,就像十进制不能准确的表示1/3(1/3=0.3333..),也就是说二进制表示小数的时候只能够表示能够用 1/(2^n) 的和 的任意组合,但是0.1不能够精确表示,因为它不能够表示成为 1/(2^n)的和 的形式。

可以看到在Java中进行浮点数运算的时候,会出现丢失精度的问题。那么我们如果在进行商品价格计算的时候,就会出现问题。很有可能造成我们手中有0.06元,却无法购买一个0.05元和一个0.01元的商品。因为如上所示,他们两个的总和为0.060000000000000005。这无疑是一个很严重的问题,尤其是当电商网站的并发量上去的时候,出现的问题将是巨大的。可能会导致无法下单,或者对账出现问题。

而 Decimal 是 精确计算 ,所以一般牵扯到金钱的计算,都使用 Decimal。

3. 面向对象的设计原则你知道有哪些吗

面向对象编程中的六大原则:

  • 单一职责原则(SRP):一个类应该只有一个引起它变化的原因,即一个类应该只负责一项职责。例子:考虑一个员工类,它应该只负责管理员工信息,而不应负责其他无关工作。
  • 开放封闭原则(OCP):软件实体应该对扩展开放,对修改封闭。例子:通过制定接口来实现这一原则,比如定义一个图形类,然后让不同类型的图形继承这个类,而不需要修改图形类本身。
  • 里氏替换原则(LSP):子类对象应该能够替换掉所有父类对象。例子:一个正方形是一个短形,但如果修改一个短形的高度和宽度时,正方形的行为应该如何改变就是一个违反里氏替换原则的例子。
  • 接口隔离原则(ISP):客户端不应该依赖那些它不需要的接口,即接口应该小而专。例子:通过接口抽象层来实现底层和高层模块之间的解耦,比如使用依赖注入。
  • 依赖倒置原则(DIP):高层模块不应该依赖低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。例子: 如果一个公司类包含部门类,应该考虑使用合成/聚合关系,而不是将公司类继承自部门类。
  • 最少知识原则 (Law of Demeter):一个对象应当对其他对象有最少的了解,只与其直接的朋友交互。

重写和重载的区别

(1)重写:

  • 在继承关系中,子类重写父类的方法(重写父子方法签名参数必须相同;方法签名:方法名、返回值类型、参数)
  • 修饰符:子类大于父类修饰符 public > protected > default > private
  • 异常抛出:父类有抛出异常,子类抛出的异常<=父类抛出的异常;父类没有抛出异常,子类只能抛出运行时异常
  • 重写是一个运行期间概念,即在运行的时候,根据引用变量所指向的实际对象的类型来调用方法。
  • 目的:实现多态性,通过父类引用调用子类对象的方法,实现对同一方法名的不同实现。

(2)重载:

  • 在同一个类中,同名不同参(参数个数、参数类型、参数顺序可以不同)对访问修饰没有特殊要求
  • 重载是一个编译期概念,即在编译时根据参数变量的类型判断应该调用哪个方法。
  • 目的:增加方法的灵活性和可读性,让同一个方法名可以对不同情况进行处理。

4. 非静态内部类和静态内部类的区别?

区别包括:

  • 非静态内部类依赖于外部类的实例,而静态内部类不依赖于外部类的实例。
  • 非静态内部类可以访问外部类的实例变量和方法,而静态内部类只能访问外部类的静态成员。
  • 非静态内部类不能定义静态成员,而静态内部类可以定义静态成员。
  • 非静态内部类在外部类实例化后才能实例化,而静态内部类可以独立实例化。
  • 非静态内部类可以访问外部类的私有成员,而静态内部类不能直接访问外部类的私有成员,需要通过实例化外部类来访问。

非静态内部类可以直接访问外部方法,编译器是怎么做到的?

非静态内部类可以直接访问外部方法是因为编译器在生成字节码时会为非静态内部类维护一个指向外部类实例的引用。

这个引用使得非静态内部类能够访问外部类的实例变量和方法。编译器会在生成非静态内部类的构造方法时,将外部类实例作为参数传入,并在内部类的实例化过程中建立外部类实例与内部类实例之间的联系从而实现直接访问外部方法的功能,

5. 代码块——相当于另一种形式的构造器(对构造器的补充机制,可做初始化操作)

代码块 又称为 初始化块,属于类中的成员,即是类的一部分,类似于方法,将逻辑语句封装在方法体中,通过{}包围起来,但和方法不同,没有方法名,没有返回没有参数,只有方法体,而且不通过对象或类显式调用, 而是加载类时,或创建对象时,隐式调用

java
语法:
[static]{
        任意代码
        }[;]

这里的[]表示可选,一般分号写;有static修饰的代码块叫静态代码块,否则是普通代码块

应用场景:如果多个构造器中都有重复的 语句,可以抽取到初始化块中,提高代码对的重用性

不管 调用哪个构造器,都会调用代码块的内容,代码块的调用优先于构造器

java
{
    System.out.println("电影屏幕放映");
    System.out.println("广告开始了");
    System.out.println("电影正式开始了");
}

public Movie(String name) {
    this.name = name;
}

使用细节:

  1. static代码块

    作用是对类进行初始化,随着类的加载而执行,并且 只执行一次

    如果是 普通代码块,则 每创建一个对象就执行一次

  2. 类什么时候被加载?(重要)

    • 创建对象实例时(new)
    • 创建子类对象实例,父类也会被加载(如果有继承关系,父类的代码块先加载,因为构造器的优先顺序也是如此
    • 使用类的 静态成员 时(静态属性,静态方法)
  3. 普通的代码块:

    只有在创建对象实例时,会被隐式的调用,创建一次调用一次

    如果只是使用类的静态成员时,普通代码块并不会执行。它和类的加载没有毛关系

总结——创建一个对象时执行的顺序

  1. 调用静态代码块和静态属性初始化

    静态代码块和静态属性初始化调用的优先级一样,如果有多个静态代码块和多个静态变量初始化,则按他们的定义顺序调用

    与加载相关

  2. 调用普通代码块和普通属性的初始化

    普通代码块和普通属性初始化调用的优先级一样,如果有多个普通代码块和多个普通属性初始化,则按他们的定义顺序调用

  3. 调用构造方法

    java
    public class test {
        public static void main(String[] args) {A a = new A();}
    }
    class A{
        private int n2 = getN2();
        {
            System.out.println("A 普通代码块02");
        }
        private static int n1 = getN1();
        static {
            System.out.println("A 静态代码块01");
        }
        
        public int getN2(){
            System.out.println("getN2被调用");
            return 100;
        }
        public static int getN1(){
            System.out.println("getN1被调用");
            return 100;
        }
        public A() {
            System.out.println("无参构造器被调用");;
        }
    }
    输出:
        A 静态代码块01
    	getN1被调用
        getN2被调用
    	A 普通代码块02
        无参构造器被调用

    总结一下:类加载时,先加载与类相关(静态)-> 加载 普通 属性 -> 构造器

6. 深入总结——创建一个子类对象时(继承关系)

WARNING

构造方法其实被 隐式 地调用了 super()调用普通代码块

  1. 父类的静态代码块和静态属性(优先级一样,按定义顺序)

  2. 子类的静态代码块和静态属性(优先级一样,按定义顺序)

    ps:创建对象前现需要加载类,先加载 父类 然后加载 子类,这个过程就是 先把父类的静态相关 加载出来然后把 * 子类的静态相关* 加载出来

  3. 父类的普通代码块和普通属性初始化(优先级一样,按定义顺序)

    ps:这里子类构造器中隐式的 super 开始执行

    于是到达父类构造器,然后父类构造器中隐式的 super 执行(这里不再考虑上一级),因此执行父,即 **普通代码块和普通属性初始化 **

  4. 父类的构造方法

    ps:父类构造器隐式的语句执行完毕,真正执行构造器中的语句

  5. 子类的普通代码块和普通属性初始化(优先级一样,按定义顺序)

    ps:子类构造器中的super语句执行完毕,回到子类构造器执行子类第二个隐式的调用,即普通代码块和普通属性初始化

  6. 子类的构造方法

    ps:执行子类构造器中的语句

7. 深拷贝和浅拷贝

浅拷贝是指只复制对象本身和其内部的值类型字段,但不会复制对象内部的引用类型字段。换句话说浅拷贝只是创建一个新的对象,然后将原对象的字段值复制到新对象中,但如果原对象内部有引用类型的字段,只是将引用复制到新对象中,两个对象指向的是同一个引用对象。

  • 深拷贝是指在复制对象的同时,将对象内部的所有引用类型字段的内容也复制一份,而不是共享引用。换句话说,深拷贝会递归复制对象内部所有引用类型的字段,生成一个全新的对象以及其内部的所有对家。

8. 在 Java 中实现单例模式(懒汉模式和饿汉模式)考虑多线程的环境。

1. 懒汉模式(Lazy Initialization)

懒汉模式的特点是延迟实例化,即只有在第一次使用时才创建对象。为了保证线程安全,可以使用双重检查锁定(Double-Checked Locking),配合 volatile 关键字来确保变量的可见性。

java
public class SingletonLazy {
    // 使用 volatile 确保多线程间的可见性
    private static volatile SingletonLazy instance = null;

    // 私有构造函数,防止外部实例化
    private SingletonLazy() {}

    // 提供线程安全的全局访问点
    public static SingletonLazy getInstance() {
        if (instance == null) {
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy(); // 懒加载
                }
            }
        }
        return instance;
    }
}

解释:

  • 使用 volatile 保证 instance 对所有线程的可见性,防止指令重排序。
  • 双重检查锁定(if (instance == null))确保只在实例未初始化时才进入同步块,提升性能。

2. 饿汉模式(Eager Initialization)

饿汉模式的特点是类加载时就创建实例,不管是否使用,故线程天生安全。但这种方式在某些情况下可能会造成不必要的资源浪费(比如程序中没有用到该实例)。

java
public class SingletonEager {
    // 类加载时立即创建对象
    private static final SingletonEager instance = new SingletonEager();

    // 私有构造函数,防止外部实例化
    private SingletonEager() {}

    // 提供全局访问点
    public static SingletonEager getInstance() {
        return instance;
    }
}

解释:

  • 实例在类加载时创建,利用类加载机制保证线程安全。
  • 没有涉及到同步开销,调用速度快,但不管是否使用都会创建实例。

9. 自动装箱的弊端

自动装箱有一个问题,那就是在一个循环中进行自动装箱操作的情况,如下面的例子就会创建多余的对象,影响程序的性能。

java
Integer sum=0;
for(int i=1000;i<5000;i++){Sum+=i;}

上面的代码 sum += i 可以看成 sum = sum + i ,但是 +这个操作符不适用于Integer对象,首先sum进行自动拆箱操作,进行数值相加操作,最后发生自动装箱操作转换成Integer对象。其内部变化如下

java
int result = sum.intValue() + i; 
Integer sum = new Integer(result);

由于我们这里声明的sum为Integer类型,在上面的循环中会创建将近4000个无用的Integer对象,在这样庞大的循环中,会降低程序的性能并且加重了垃圾回收的工作量。因此在我们编程时,需要注意到这一点,正确地声明变量类型,避免因为自动装箱引起的性能问题。

10. Java为什么要有Integer

Integer对应是int类型的包装类,就是把int类型包装成Object对象,对象封装有很多好处,可以把属性也就是数据跟处理这些数据的方法结合在一起,比如Integer就有parselnt0等方法来专门处理int型相关的数据。

另一个非常重要的原因就是在Java中绝大部分方法或类都是用来处理类类型对象的,如Arraylist集合类就只能以类作为他的存储对象,而这时如果想把一个int型的数据存入list是不可能的,必须把它包装成类也就是Integer才能被List所接受。所以Integer的存在是很必要的。

泛型中的应用

在Java中,泛型只能使用引用类型,而不能使用基本类型。因此,如果要在泛型中使用int类型,必须使用Integer包装类。例如,假设我们有一个列表,我们想要将其元素排序,并将排序结果存储在一个新的列表中。如果我们使用基本数据类型int,无法直接使用 Collections.sort()方法。但是,如果我们使用Integer包装类,我们就可以轻松地使用 Collections.sort()方法。

java
List<Integer>list =new ArrayList<>();
list.add(3);
list.add(1);
list.add(2);
Collections.sort(list);
System.out.println(list);

转换中的应用

在Java中,基本类型和引用类型不能直接进行转换,必须使用包装类来实现。例如,将一个int类型的值转换为String类型,必须首先将其转换为Integer类型,然后再转换为String类型。

java
int i = 10;
Integer integer = new Integer(i);
String str=integer.tostring();
System.out.println(str);

集合中的应用

Java集合中只能存储对象,而不能存储基本数据类型 。因此,如果要将int类型的数据存储在集合中,必须使用Integer包装类。例如,假设我们有一个列表,我们想要计算列表中所有元素的和。如果我们使用基本数据类型int,我们需要使用一个循环来遍历列表,并将每个元素相加。但是,如果我们使用Integer包装类,我们可以直接使用stream() 方法来计算所有元素的和。

java
List<Integer>list = new ArrayList<>();
list.add(3);
list.add(1);
list.add(2);
int sum = list.stream().mapToInt(Integer::intValue).sum();
System.out.println(sum);

Integer相比int有什么优点?

int是Java中的原始数据类型,而Integer是int的包装类。

Integer和 int 的区别:

基本类型和引用类型 : 首先,int是一种基本数据类型,而Integer是一种引用类型。基本数据类型是Java中最基本的数据类型,它们是预定义的,不需要实例化就可以使用。而引用类型则需要通过实例化对象来使用。 这意味着,使用int来存储一个整数时,不需要任何额外的内存分配,而使用Integer时,必须为对象分配内存 。在性能方面,基本数据类型的操作通常比相应的引用类型快。

自动装箱和拆箱 : 其次,Integer作为int的包装类,它可以实现自动装箱和拆箱。 自动装箱是指将基本类型转化为相应的包装类类型,而自动拆箱则是将包装类类型转化为相应的基本类型 。这使得Java程序员更加方便地进行数据类型转换。例如,当我们需要将int类型的值赋给Integer变量时,Java可以自动地将int类型转换为Integer类型。同样地,当我们需要将Integer类型的值赋给int变量时,Java可以自动地将Integer类型转换为int类型。

空指针异常 : 另外,int变量可以直接赋值为0,而Integer变量必须通过实例化对象来赋值。 如果对一个未经初始化的Integer变量进行操作,就会出现空指针异常。这是因为它被赋予了nul值,而null值是无法进行自动拆箱的

那为什么还要保留int类型?

包装类是引用类型,对象的引用和对象本身是分开存储的,而对于基本类型数据,变量对应的内存块直接存储数据本身。

因此,基本类型数据在读写效率方面,要比包装类高效。除此之外,在64位M上,在开启引用压缩的情况下,一个nteger对象占用16个字节的内存空间,而一个int类型数据只占用4字节的内存空间,前者对空间的占用是后者的4倍。

也就是说,不管是读写效率,还是存储效率,基本类型都比包装类高效

说一下 integer的缓存

Java的Integer类内部实现了一个静态缓存池,用于存储特定范围内的整数值对应的Integer对象。

默认情况下,这个范围是-128至127。当通过Integer.valueOf(int) 方法创建一个在这个范围内的整数对象时,并不会每次都生成新的对象实例,而是复用缓存中的现有对象,会直接从内存中取出,不需要新建个对象。

11. 怎么理解面向对象?简单说说封装继承多态

面向对象是一种编程范式,它将现实世界中的事物抽象为对象,对象具有属性(称为字段或属性)和行为(称为方法) 。面向对象编程的设计思想是以对象为中心,通过对象之间的交互来完成程序的功能,具有灵活性和可扩展性,通过封装和继承可以更好地应对需求变化。

Java面向对象的三大特性包括: 封装、继承、多态:

封装:

封装是指将对象的属性(数据)和行为(方法)结合在一起,对外隐藏对象的内部细节,仅通过对象提供的接口与外界交瓦。封装的目的是增强安全性和简化编程,使得对象更加独立

继承:

继承是一种可以使得子类自动共享父类数据结构和方法的机制。它是代码复用的重要手段,通过继承可以建立类与类之间的层次关系,使得结构更加清晰。

多态:

多态是指允许不同类的对象对同一消息作出响应。即同一个接口,使用不同的实例而执行不同操作。多态性可以分为编译时多态(重载) 和运行时多态(重写)。它使得程序具有良好的灵活性和扩展性

补充

多态体现在哪几个方面?

多态在面向对象编程中可以体现在以下几个方面:

  • 方法重载 : 方法重载是指同一类中可以有多个同名方法,它们具有不同的参数列表(参数类型、数量或顺序不同) 。虽然方法名相同,但根据传入的参数不同,编译器会在编译时确定调用哪个方法。示例:对于一个 add 方法,可以定义为 add(int a,int b)和 add(double a,double b)。

  • 方法重写 : 方法重写是指子类能够提供对父类中同名方法的具体实现。在运行时,JVM会根据对象的实际类型确定调用哪个版本的方法。这是实现多态的主要方式。 示例:在一个动物类中,定义一个 sound 方法,子类Dog可以重写该方法以实现 bark,而Cat 可以实现meow。

  • 接口与实现 : 多态也体现在接口的使用上,多个类可以实现同一个接口,并且用接口类型的引用来调用这些类的方法。这使得程序在面对不同具体实现时保持一贯的调用方式。 示例:多个类(如Dog,cat )都实现了一个Animal接口,当用 Animal 类型的引用来调用 makesound 方法时,会触发对应的实现。

  • 向上转型和向下转型 : 在Java中,可以使用父类类型的引用指向子类对象,这是 向上转型。通过这种方式,可以在运行时期采用不同的子类实现。

    向下转型 是将父类引用转回其子类类型,但在执行前需要确认引用实际指向的对象类型以避免 ClassCastException

多态解决了什么问题?

多态是指子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。多态这种特性也需要编程语言提供特殊的语法机制来实现,比如继承、接口类。

多态可以提高代码的扩展性和复用性,是很多设计模式、设计原则、编程技巧的代码实现基础。比如策略模式、基于接口而非实现编程、依赖倒置原则、里式替换原则、利用多态去掉冗长的 if-else 语句等等

12. 抽象类和普通类区别?

  • 实例化:普通类可以直接实例化对象,而抽象类不能被实例化,只能被继承。

    继承了抽象类的子类在实例化的时候,会调用抽象类的构造器,但这不意味着抽象类被实例化了

  • 方法实现:普通类中的方法可以有具体的实现,而抽象类中的方法可以有实现也可以没有实现。

  • 继承:一个类可以继承一个普通类,而且可以继承多个接口;而一个类只能继承一个抽象类,但可以同时实现多个接口。

  • 实现限制:普通类可以被其他类继承和使用,而抽象类一般用于作为基类,被其他类继承和扩展使用。

13. Java抽象类和接口的区别是什么?

抽象类用于 描述类的共同特性和行为,可以有成员变量、构造方法和具体方法。适用于 有明显继承关系 的场景。

接口用于 定义行为规范,可以多实现,只能有常量和抽象方法(java8 以后可以有默认方法和静态方法)。适用于 **定义类的能力或功能 **。

两者的区别:

实现方式 : 实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。

方法方式 : 接口只有定义,不能有方法的实现,java1.8中可以定义default方法体和静态方法,jdk9可以引入私有方法 (用于在接口中为默认方法或者其他私有方法提供辅助功能,这些方法不能被实现类访问,只能在接口内部使用),而抽象类可以有定义与实现,方法可在抽象类中实现。

java
public interface Animal {
    void makeSound();
    
    default void sleep() {
        System.out.println("Sleeping...");
        logSleep();
    }
    
    private static logSleep(){
        System.out.println("Logging sleep");
    }
}

访问修饰符:

接口成员变量默认为 public static final,必须赋初值,不能被修改;其所有的成员方法都是 public abstract 的,可以省略。

抽象类中成员变量默认default,可在子类中被重新定义,也可被重新赋值;抽象方法被 abstract 修饰,不能被private、static、synchronized和native等修饰,必须以分号结尾,不带花括号。

变量 : 抽象类可以包含实例变量和静态变量,而接口只能包含常量(即静态常量)

14. 泛型

泛型是 Java 编程语言中的一个重要特性,它允许类、接口和方法在定义时使用一个或多个类型参数,这些类型参数在使用时可以被指定为具体的类型。

泛型的主要目的是在编译时提供更强的类型检查,并且在编译后能够保留类型信息,避免了在运行时出现类型转换异常。

泛型的上下限,对传入的泛型类型实参进行上下边界的限制

比如限制必须是某个类的父类,限制必须是某个类的子类,这就是上下限的限制

java
class Info<T extends Number>{
    ...
}

public static void fun(Info<? super String> temp){
    ...
}

可以有泛型接口,泛型类,泛型方法

java
class Point<T>{} 
class Note<K,V>{}
// 泛型类,实例化的时候需要指定泛型,比如 new Point<String>();

泛型接口

java
interface Notepad<T>{
    public T getVar();
}

class NotepadImpl<T> implements Notepad<T>{
    private T var;
    
    public T getVar(){
        
    }
}

/// new NotepadImpl<String>();

泛型方法,有固定的使用方式:为什么要用泛型方法?泛型类必须在实例化时指定类型,如果想换一种类型必须重新new,不灵活,所以可以使用泛型方法

java
class Generic{    
    public <T> T getObject(Class<T> c) throws InstantiationException, IllegalAccessException{
        T t = c.newInstance();
        return t;
    } 
}
// 调用
Generic generic = new Generic();
Object obj = generic.getObject(Class.forName("全类名"));

15. Java 创建对象有哪些方式?

  • new
  • 使用 Class 类的 newInstance() 方法
  • 使用 Constructor 类的 newInstance() 方法
  • 使用 clone() 方法(实现了Cloneable接口)
  • 使用反序列化:将对象序列化到文件或者流中,然后反序列化

16. New出的对象什么时候回收?

通过过关键字 new 创建的对象,由Java的垃圾回收器(Garbage Colector)负责回收。垃圾回收器的工作是在程序运行过程中自动进行的,它会周期性地检测不再被引用的对象,并将其回收释放内存。

具体来说,Java对象的回收时机是由垃圾回收器根据一些算法来决定的,主要有以下几种情况:

  1. 引用计数法:某个对象的引用计数为0时,表示该对象不再被引用,可以被回收。
  2. 可达性分析算法:从根对象(如方法区中的类静态属性、方法中的局部变量等) 出发,通过对象之间的引用链进行遍历,如果存在一条引用链到达某个对象,则说明该对象是可达的,反之不可达,不可达的对象将被回收。
  3. 终结器(Finalizer):如果对象重写了 finalize()方法,垃圾回收器会在回收该对象之前调用finalize()方法,对象可以在 finalize() 方法中进行一些清理操作。然而,终结器机制的使用不被推荐,因为它的执行时间是不确定的,可能会导致不可预测的性能问题。

17. 什么是反射?

Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类中的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java 语言的反射机制。

反射具有以下特性:

  1. 运行时类信息访问:反射机制允许程序在运行时获取类的完整结构信息,包括类名、包名、父类、实现的接口、构造函数、方法和字段等。
  2. 动态对象创建:可以使用反射API动态地创建对象实例,即使在编译时不知道具体的类名。这是通过 Class类的newInstance() 方法或Constructor对象的newInstance0方法实现的。
  3. 动态方法调用:可以在运行时动态地调用对象的方法,包括私有方法。这通过Method类的invoke()方法实现,允许你传入对象实例和参数值来执行方法。
  4. 访问和修改字段值:反射还允许程序在运行时访问和修改对象的字段值,即使是私有的。这是通过Field类的get()和set() 方法完成的。(首先要用setAccessable设置为true,这样私有属性的访问限制就会被打破)

反射在你平时写代码或者框架中的应用场景有哪些?

加载数据库驱动

我们的项目底层数据库有时是用mysql,有时用oracle,需要 动态地根据实际情况加载驱动类

这时候我们在使用 JDBC 连接数据库时使用 Class.forName() 通过反射加载数据库的驱动程序,如果是mysql则传入mysql的驱动类,而如果是oracle则传入的参数就变成另一个了,

java
//DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
Class.forName("com.mysgl.cj.jdbc.Driver");

配置文件加载

Spring 框架的 IOC(动态加载管理 Bean),Spring通过配置文件配置各种各样的bean,你需要用到哪些bean就配哪些,spring容器就会根据你的需求去动态加载,你的程序就能健壮地运行。

Spring通过XML配置模式装载Bean的过程:

  • 将程序中所有XML或properties配置文件加载入内存
  • Java类里面解析xml或者properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息
  • 使用反射机制,根据这个字符串获得某个类的Class实例
  • 动态配置实例的属性

getName、getCanonicalName与getSimpleName的区别?

  • getSimpleName:只获取类名
  • getName:类的全限定名,jvm中Class的表示,可以用于动态加载Class对象,例如Class.forName。
  • getCanonicalName:返回更容易理解的表示,主要用于输出(toString)或log打印,大多数情况下和getName一样,但是在内部类、数组等类型的表示形式就不同了。

18. 能讲讲Java注解的原理吗?

注解本质是一个 继承Annotation 的特殊 接口,其 具体实现类是Java运行时生成的动态代理类

我们 通过反射获取注解时,返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用 AnnotationlnvocationHandlerinvoke 方法。该方法会从 memberValues 这个 Map 中索引出对应的值。而 memberValues 的来源是Java常量池

Java注解的作用域?

注解的作用域(Scope)指的是注解可以应用在哪些程序元素上,例如类、方法、字段等。Java注解的作用域可以分为三种:

  1. 类级别作用域:用于描述类的注解,通常放置在类定义的上面,可以用来指定类的一些属性,如类的访问级别. 继承关系、注释等。
  2. 方法级别作用域:用于描述方法的注解,通常放置在方法定义的上面,可以用来指定方法的一些属性,如方法的访问级别、返回值类型、异常类型、注释等。
  3. 字段级别作用域:用于描述字段的注解,通常放置在字段定义的上面,可以用来指定字段的一些属性,如字段的访问级别、默认值、注释等。
  4. 除了这三种作用域,Java还提供了其他一些注解作用域,例如构造函数作用域和局部变是作用域。这些注解作用域可以用来对构造函数和局部变量进行描述和注释。

19. 介绍一下Java异常

Java的异常体系主要基于两大类:Throwable类及其子类。Throwable有两个重要的子类:Error和Exception,它们分别代表了不同类型的异常情况。

  1. Error(错误):表示运行时环境的错误。错误是程序无法处理的严重问题 ,如系统崩溃、虚拟机错误、动态链接失败等。通常,程序不应该尝试捕获这类错误。例如,0utOfMemoryError、StackOverflowError等

  2. Exception(异常):表示程序本身可以处理的异常条件

    异常分为两大类:

    非运行时异常:这类异常在编译时期就必须被捕获或者声明抛出。它们通常是外部错误,如文件不存在(FileNotFoundException) 、类未找到(ClassNotFoundException)等。非运行时异常强制程序员处理这些可能出现的问题,增强了程序的健壮性。

    运行时异常:运行时异常由程序错误导致如空指针访问(NullPointerException)、数组越界(ArrayIndexOutOfBoundsException) 等。运行时异常是不需要在编译时强制捕获或声明的。

20. try{return “a”} fianlly{return“b”}这条语句返回啥

finally 块中的 return 语句会 覆盖 try 块中的 return 返回,因此,该语句将返回”b”

21. ==与 equals 有什么区别?

对于字符串变量来说,使用"==”和"equals"比较字符串时,其比较方法不同。"=="比较两个变量本身的值,即两个对象在内存中的首地址,“ equals”比较字符串包含内容是否相同。

对于非字符串变量来说,如果没有对 equals() 进行重写的话,"=="和"equals ”方法的作用是相同的,都是用来比较对象在堆内存中的首地址,即用来比较两个引用变量是否指向同一个对象。

==:比较的是两个字符串内存地址(堆内存)的数值是否相等,属于 数值 比较

equals():比较的是两个字符串的内容,属于 内容 比较。

22. StringButter机StringBuild区别是什么?

  1. 可变与不可变
    1. String类中使用字符数组char[]保存字符串,因为有“final”修饰符,所以String对象是不可变的。每次对String的操作,是一种覆盖,都会生成新的String对象
    2. StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,这两种对象都是可变的。
  2. 线程安全
    1. String中的对象是不可变的,也就可以理解为常量,显然线程安全;
    2. StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的;synchronized
    3. StringBuilder是非线程安全的。
  3. 执行效率
    1. String最慢, 因为每次对 String 类型进行改变的时候其实都等同于在堆中生成了一个新的 String 对象,然后将指针指向新的 String 对象,这样不仅效率低下,而且大量浪费有限的内存空间。
    2. StringBuilder的效率会比StringBuffer更高些。

23. Stream流的并行API是什么?

ParallelStream

并行流(ParallelStream)就是将源数据分为多个子流对象进行多线程操作,然后将处理的结果再汇总为一个流对象,底层是使用通用的 fork/join 池来实现,即将一个任务拆分成多个“小任务“并行计算,再把多个“小任务”的结果合并成总的计算结果

对CPU密集型的任务来说,并行流使用 ForkoinPool 线程池,为每个CPU分配一个任务,这是非常有效率的,但是如果任务不是CPU密集的,而是I/O密集的,并且任务数相对线程数比较大,那么直接用Parallelstream并不是很好的选择。

24. 怎么把一个对象从一个jvm转移到另一个jvm?

使用 序列化和反序列化: 将对象序列化为字节流,并将其发送到另一个 JVM,然后在另一个 JVM 中反序列化字节流恢复对象。这可以通过 Java 的 ObjectOutputStreamObjectlnputStream 来实现。

使用 消息传递机制 : 利用消息传递机制,比如使用消息队列(如 RabbitMQ、Kafka)或者通过网络套接字进行通信,将对象从一个 JVM 发送到另一个 JVM。这需要自定义协议来序列化对象并在另一个 JVM 中反序列化。

使用 远程方法调用(RPC) : 可以使用远程方法调用框架来实现对象在不同 JVM 之间的传输。远程方法调用可以让你在分布式系统中调用远程 JM 上的对象的方法。

使用 共享数据库或缓存 : 将对象存储在共享数据库(如 MySQL、PostgreSQl)或共享缓存(如 Redis)中,让不同的 JVM 可以访问这些共享数据。这种方法适用于需要共享数据但不需要直接传输对象的场景,

25. 序列化和反序列化让你自己实现你会怎么做?

Java 默认的序列化虽然实现方便,但却 存在安全漏洞、不跨语言 以及性能差等缺陷。

  • 无法跨语言 : Java 序列化目前只适用基于 Java 语言实现的框架,其它语言大部分都没有使用 Java 的席列化框架,也没有实现Java 序列化这套协议。因此,如果是两个基于不同语言编写的应用程序相互通信,则无法实现两个应用服务之间传输对象的序列化与反序列化。
  • 容易被攻击 : Java 序列化是不安全的,我们知道对象是通过在 ObjectlnputStream 上调用 readObject() 方法进行反序列化的,这个方法其实是一个神奇的构造器,它可以将类路径上几乎所有实现了 Serializable 接口的对象都实例化。这也就意味着,在反序列化字节流的过程中,该方法可以执行任意类型的代码,这是非常危险的。
  • 序列化后的流太大 : 序列化后的二进制流大小能体现序列化的性能。序列化后的二进制数组越大,占用的存储空间就越多,存储硬件的成本就越高。如果我们是进行网络传输,则占用的带宽就更多,这时就会影响到系统的吞吐量。

我会考虑用主流序列化框架,比如 FastJson、Protobuf来替代 Java 序列化。

如果追求性能的话,Protobuf席列化框架会比较合适,Protobuf 的这种数据存储格式,不仅压缩存储数据的效果好,在编码和解码的性能方面也很高效。Protobuf的编码和解码过程结合.proto 文件格式,加上 Protocol Buffer 独特的编码格式,只需要简单的数据运算以及位移等操作就可以完成编码与解码。可以说 Protobuf 的整体性能非常优秀。

26. 将对象转为二进制字节流具体怎么实现?

其实,像序列化和反序列化,无论这些可逆操作是什么机制,都会有对应的 处理和解析协议,例如 加密和解密 ,TCP的粘包和拆包,序列化机制是通过序列化协议来进行处理的,和 class 文件类似,它其实是定义了序列化后的字节流格式,然后对此格式进行操作,生成符合格式的字节流或者将字节流解析成对象

在Java中通过序列化对象流来完成序列化和反序列化:

  • ObjectOutputStream:通过writeObject()方法做序列化操作
  • ObjectInputStream:通过readObject0方法做反序列化操作。

只有实现了Serializable或Externalizable接口的类的对象才能被序列化,否则抛出异常!

实现对象序列化:

  1. 让类实现Serializable接口:

    java
    import java.io.serializable;
    public class Myclass implements Serializable{}// class code
  2. 创建输出流并写入对象:

    java
    import java.io.FileOutputStream;
    import java.io.ObjectOutputStream;
    
    MyClass obj = new Myclass();
    try {
        FileOutputStream fileOut = new FileOutputstream("object.ser");
        ObjectOutputstream out = new objectOutputstream(fileOut);
        out.writeobject(obj);
        out.close();
        fileOut.close();
    }catch(IException e){
        e.printstackTrace();
    }

实现对象反序列化:

创建输入流并读取对象:

java
import java.io.FileInputstream;
import java.io.0bjectInputstream;

MyClass newobj= null;
try {
    FileInputstream fileIn = new FileInputstream("object.ser");
    ObjectInputStream in = new ObjectInputstream(fileIn);
    newobj=(Myclass)in.readobject();
    in.close();
    fileIn.close();
}catch(IExceptionclassNotFoundException e){
    e.printstackTrace();
}

通过以上步骤,对象obi会被序列化并写入到文件"obiect.ser" 中,然后通过反序列化操作,从文件中读取字节流并恢复为对象newObj。这种方式可以方便地将对象转换为字节流用于持久化存储、网络传输等操作。需要注意的是,要确保类实现了Serializable接口,并且所有成员变量都是Serializable的才能被正确序列化。

27. a = a + b 与 a += b 的区别

+= 隐式的将加操作的结果类型强制转换为持有结果的类型。如果两个整型相加,如 byte、short 或者 int,首先会将它们提升到 int 类型,然后在执行加法操作。

java
byte a = 127;
byte b = 127;
b = a + b; // error : cannot convert from int to byte
b += a; // ok

(因为 a+b 操作会将 a、b 提升为 int 类型,所以将 int 类型赋值给 byte 就会编译出错)

java
private static<T extends Number> double add(T a, T b){
 	return a.doubleValue() + b.doubleValue();
}

28. Java7的try-with-resource

资源实现了AutoCloseable接口,可以使用try-with-resource,如果你在try子句打开资源,会在try代码快执行后或者异常处理后自动关闭资源

29. 异常的底层

提到JVM处理异常的机制,就需要提及Exception Table,以下称为异常表。我们暂且不急于介绍异常表,先看一个简单的 Java 处理异常的小例子。

java
public static void simpleTryCatch() {
   try {
       testNPE();
   } catch (Exception e) {
       e.printStackTrace();
   }
}

使用javap来分析这段代码(需要先使用javac编译)

java
//javap -c Main
 public static void simpleTryCatch();
    Code:
       0: invokestatic  #3                  // Method testNPE:()V
       3: goto          11
       6: astore_0
       7: aload_0
       8: invokevirtual #5                  // Method java/lang/Exception.printStackTrace:()V
      11: return
    Exception table:
       from    to  target type
           0     3     6   Class java/lang/Exception

看到上面的代码,应该会有会心一笑,因为终于看到了Exception table,也就是我们要研究的异常表。

异常表中包含了一个或多个异常处理者(Exception Handler)的信息,这些信息包含如下

  • from 可能发生异常的起始点
  • to 可能发生异常的结束点
  • target 上述from和to之前发生异常后的异常处理者的位置
  • type 异常处理者处理的异常的类信息

30. SPI机制

什么是SPI机制?

SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 解耦

SPI整体机制图如下:

img

当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/ 目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的 META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查找服务的实现的工具类是: java.util.ServiceLoader

SPI机制的应用?

  • SPI机制 - JDBC DriverManager

在JDBC4.0之前,我们开发有连接数据库的时候,通常会用Class.forName("com.mysql.jdbc.Driver")这句先加载数据库相关的驱动,然后再进行获取连接等的操作。 **而JDBC4.0之后不需要用Class.forName("com.mysql.jdbc.Driver")来加载驱动,直接获取连接就可以了,现在这种方式就是使用了Java的SPI扩展机制来实现 **。

  • JDBC接口定义

首先在java中定义了接口java.sql.Driver,并没有具体的实现,具体的实现都是由不同厂商来提供的。

  • mysql实现

在mysql的jar包mysql-connector-java-6.0.6.jar中,可以找到META-INF/services目录,该目录下会有一个名字为java.sql.Driver 的文件,文件内容是com.mysql.cj.jdbc.Driver,这里面的内容就是针对Java中定义的接口的实现。

  • postgresql实现

同样在postgresql的jar包postgresql-42.0.0.jar中,也可以找到同样的配置文件,文件内容是org.postgresql.Driver ,这是postgresql对Java的java.sql.Driver的实现。

  • 使用方法

上面说了,现在使用SPI扩展来加载具体的驱动,我们在Java中写连接数据库的代码的时候,不需要再使用 Class.forName("com.mysql.jdbc.Driver")来加载驱动了,而是直接使用如下代码:

java
String url = "jdbc:xxxx://xxxx:xxxx/xxxx";
Connection conn = DriverManager.getConnection(url,username,password);
.....

####SPI机制的简单示例?

我们现在需要使用一个内容搜索接口,搜索的实现可能是基于文件系统的搜索,也可能是基于数据库的搜索。

  • 先定义好接口
java
public interface Search {
    public List<String> searchDoc(String keyword);   
}
  • 文件搜索实现
java
public class FileSearch implements Search{
    @Override
    public List<String> searchDoc(String keyword) {
        System.out.println("文件搜索 "+keyword);
        return null;
    }
}
  • 数据库搜索实现
java
public class DatabaseSearch implements Search{
    @Override
    public List<String> searchDoc(String keyword) {
        System.out.println("数据搜索 "+keyword);
        return null;
    }
}
  • resources 接下来可以在resources下新建META-INF/services/目录,然后新建接口全限定名的文件: com.cainiao.ys.spi.learn.Search,里面加上我们需要用到的实现类
com.cainiao.ys.spi.learn.FileSearch
  • 测试方法
java
public class TestCase {
    public static void main(String[] args) {
        ServiceLoader<Search> s = ServiceLoader.load(Search.class);
        Iterator<Search> iterator = s.iterator();
        while (iterator.hasNext()) {
           Search search =  iterator.next();
           search.searchDoc("hello world");
        }
    }
}

可以看到输出结果:文件搜索 hello world

如果在com.cainiao.ys.spi.learn.Search文件里写上两个实现类,那最后的输出结果就是两行了。

这就是因为ServiceLoader.load(Search.class)在加载某接口时,会去META-INF/services下找接口的全限定名文件,再根据里面的内容加载相应的实现类。

这就是spi的思想,接口的实现由provider实现,provider只用在提交的jar包里的META-INF/services下根据平台定义的接口新建文件,并添加进相应的实现类内容就好

31. 克隆

以上抛出了 CloneNotSupportedException,这是因为 CloneExample 没有实现 Cloneable 接口。

应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。

32. 建造者模式实例

使用Lombok的注解@Builder即可,编译后的.class如下

java
class ComputerA {
    private final String cpu;
    private final String ram;
    private final int usbCount;
    private final String keyboard;

    ComputerA(String cpu, String ram, int usbCount, String keyboard) {
        this.cpu = cpu;
        this.ram = ram;
        this.usbCount = usbCount;
        this.keyboard = keyboard;
    }

    public static ComputerABuilder builder() {
        return new ComputerABuilder();
    }

    public static class ComputerABuilder {
        private String cpu;
        private String ram;
        private int usbCount;
        private String keyboard;

        ComputerABuilder() {
        }

        public ComputerABuilder cpu(String cpu) {
            this.cpu = cpu;
            return this;
        }

        public ComputerABuilder ram(String ram) {
            this.ram = ram;
            return this;
        }

        public ComputerABuilder usbCount(int usbCount) {
            this.usbCount = usbCount;
            return this;
        }

        public ComputerABuilder keyboard(String keyboard) {
            this.keyboard = keyboard;
            return this;
        }

        public ComputerA build() {
            return new ComputerA(this.cpu, this.ram, this.usbCount, this.keyboard);
        }

        public String toString() {
            return "ComputerA.ComputerABuilder(cpu=" + this.cpu + ", ram=" + this.ram + ", usbCount=" + this.usbCount + ", keyboard=" + this.keyboard + ")";
        }
    }
}

33. Java支持多继承吗?为什么?

不支持,多继承问题:菱形继承;接口过多

img

因为D同时继承了B和C,并且B和C又同时继承了A,那么,D中就会因为多重继承,继承到两份来自A中的属性和方法。

这时候,在使用D的时候,如果想要调用一个定义在A中的方法时,就会出现歧义。

因为这样的继承关系的形状类似于菱形,因此这个问题被形象地称为菱形继承问题。

为了解决菱形继承的问题,Java采用了“接口解决繁琐的继承,实现类解决多重继承”的原则,提供了多种机制来避免冗余代码和不确定性的问题

34. Strings 与new String有什么区别?

  1. 字符串池:
  • Strings是字符串池中的字符串,它们是在编译时就确定的,如果字符串池中已经存在相同的字符串,则直接返回池中的引用。

  • new String创建的字符串对象是在堆内存中的,每次调用new String都会创建一个新的字符串对象,即使字符串内容相同。

    java
    String s1 = "hello";
    String s2 = "hello";
    String s3 = new String("hello");
    System.out.println(s1 == s2); // true,因为s1和s2都是字符串池中的同一个对象
    System.out.println(s1 == s3); // false,因为s1和s3是不同的对象,一个在字符串池,一个在堆内存中
  1. 内存分配:
    1. Strings是常量,它们在编译时就被分配到了字符串池中。
    2. new String创建的字符串对象是在堆内存中动态分配的,它们的生命周期取决于对应的引用何时被销毁。
  2. 可变性:
    1. Strings对象是不可变的,一旦创建了就不能被修改。
    2. new String创建的字符串对象是可变的,可以通过一些方法修改字符串的值,但这些修改会创建新的字符串对象,原始的字符串对象不会被修改。
java
String s = "hello"; s = s + " world"; // 这里实际上创建了一个新的字符串对象,原始的字符串对象"hello"并没有被修改

总的来说,Strings和new String在字符串对象的创建、内存分配、可变性等方面有一些区别。在使用时需要根据具体的需求和场景选择合适的方式。通常情况下,推荐直接使用字符串常量(Strings)来创建字符串,因为它们更加高效、安全和易于理解。

35.String类能被继承吗?为什么?

在Java中,String类是被final修饰的,因此不能被继承。关键在于final关键字的作用,它表示一个类不能被继承,或者一个方法不能被子类重写。在String类中,设计者选择了将其声明为final,这意味着它的设计者认为String类应该是不可变的,并且不希望其他类来修改其行为或者添加新的行为。

这种设计有几个原因:

  1. 不可变性(Immutability):String类的不可变性使得它在多线程环境下更安全,因为它的状态不会被修改,避免了线程安全问题。
  2. 安全性:字符串常常作为参数传递给各种方法,如果字符串是可变的,那么它的值可能在传递过程中被改变,导致意外行为。
  3. 性能:由于字符串是不可变的,可以进行很多优化,例如字符串常量池,重复字符串共享等。

36. 反射中,Class.forName和ClassLoader的区别

Class.forName()和ClassLoader.loadClass()都是Java反射机制中用于动态加载类的方法,它们的作用类似,但有一些区别:

  1. Class.forName():
    1. Class.forName(String className)方法会加载并返回指定类名的Class对象。
    2. 默认情况下,Class.forName()方法会初始化被加载的类,即执行该类的静态代码块和静态初始化。
    3. Class.forName()方法还可以接受两个可选参数:第二个参数表示是否要进行初始化,默认为true;第三个参数表示用于加载类的类加载器,默认为当前线程的上下文类加载器。
Java
Class<?> clazz = Class.forName("com.example.MyClass");
  1. ClassLoader.loadClass():
  • ClassLoader.loadClass(String className)方法是ClassLoader类的方法,用于加载指定类名的Class对象。
  • 默认情况下,ClassLoader.loadClass()方法不会初始化被加载的类,只有在使用该类时才会进行初始化。
  • ClassLoader.loadClass()方法是一个受保护的方法,只能在子类中被访问。
Java
ClassLoader classLoader = getClass().getClassLoader(); Class<?> clazz = classLoader.loadClass("com.example.MyClass");
  1. 主要区别总结如下:
  • Class.forName()会自动初始化被加载的类,而ClassLoader.loadClass()不会。
  • Class.forName()是静态方法,而ClassLoader.loadClass()是实例方法,需要通过ClassLoader对象调用。
  • Class.forName()除了可以加载类,还可以指定是否初始化、指定类加载器等,而ClassLoader.loadClass()只能加载类。

在一般情况下,推荐使用ClassLoader.loadClass()方法来动态加载类,因为它更加灵活,不会默认初始化类,能够延迟类的初始化直到需要使用时。

37. 什么是Java的序列化?

Java的序列化是指将对象转换为字节流的过程,以便于将对象保存到文件、数据库中或者在网络上传输。序列化可以将对象的状态保存到字节流中,而反序列化则可以将字节流转换回对象。序列化的主要用途包括数据持久化和网络通信。

Java中的序列化由java.io.Serializable接口和java.io.ObjectOutputStream、java.io.ObjectInputStream类来实现。要使一个类可序列化,只需要实现Serializable接口,该接口是一个标记接口,不包含任何方法,它只是为了标识类的对象可以被序列化。

Java
 import java.io.Serializable;
public class MyClass implements Serializable { 
    // 类的成员变量  
    private int id;
    private String name;
    // 构造方法、成员方法等 
}

然后可以使用ObjectOutputStream将对象序列化为字节流,使用ObjectInputStream将字节流反序列化为对象:

Java
import java.io.*; 
public class SerializationDemo {   
    public static void main(String[] args) {
        // 创建对象        
        MyClass obj = new MyClass(1, "example");
        try {
            // 序列化对象到文件   
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data.txt")); 
            out.writeObject(obj);           
            out.close();             
            // 反序列化对象            
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.txt")); 
            MyClass newObj = (MyClass) in.readObject();        
            in.close();             
            
            // 使用反序列化得到的对象    
            System.out.println(newObj.getId());            
            System.out.println(newObj.getName());     
        } catch (IOException | ClassNotFoundException e) {       
            e.printStackTrace();       
        }
    } 
}

需要注意的是,为了防止对象的敏感信息被序列化,可以使用transient关键字修饰成员变量,这样这些变量在序列化过程中会被忽略。此外,序列化和反序列化过程中,被序列化的类必须保证类的版本一致,否则可能会出现反序列化失败的问题。

38. JDK动态代理与CGLIB实现的区别

JDK动态代理和CGLIB(Code Generation Library)是Java中常用的两种动态代理实现方式,它们在实现原理、适用场景和性能等方面有一些区别:

  1. 实现原理:
    1. JDK动态代理是基于接口的代理,它利用Java的反射机制在运行时动态地创建代理类,并且实现了被代理接口,代理对象与被代理对象之间的关系是委托关系。
    2. CGLIB是基于继承的代理,它通过字节码生成技术,在运行时动态地创建代理类的子类,并且重写了被代理类的方法,实现了方法的拦截和增强。
  2. 对接口的支持:
    1. JDK动态代理只能对实现了接口的类进行代理,因为它是基于接口的代理。
    2. CGLIB可以代理没有实现接口的类,因为它是基于继承的代理,通过生成目标类的子类来实现代理。
  3. 性能:
    1. JDK动态代理的性能相对较低,因为它使用Java的反射机制来调用被代理方法,涉及到反射调用,而且生成的代理类较为臃肿。
    2. CGLIB的性能相对较高,因为它是通过字节码生成技术直接调用目标类的方法,避免了反射调用,生成的代理类也比较轻量级。
  4. 适用场景:
    1. JDK动态代理适用于需要对一组接口进行统一的拦截和增强的场景,如事务管理、日志记录等。
    2. CGLIB适用于对没有实现接口的类进行代理的场景,或者需要对类的方法进行增强的场景。

总的来说,JDK动态代理和CGLIB各有优缺点,在选择使用时需要根据具体的需求和场景进行选择。如果目标类实现了接口并且对性能有要求,可以考虑使用JDK动态代理;如果目标类没有实现接口或者需要对类的方法进行增强,并且对性能要求较高,可以考虑使用CGLIB。

技术漫游

本站访客数 人次 本站总访问量