【JAVA面试】基础篇

发布于:2025-08-02 ⋅ 阅读:(14) ⋅ 点赞:(0)


字符编码和乱码问题

  1. ASCII只有128个字符,对于英文字符都可以表示。
  2. Unicode字符集(常见的Unicode Transformation Format有:UTF-7, UTF-7.5, UTF-8,UTF-16, 以及 UTF-32)
  3. UTF-8 使用一至四个字节为每个字符编码
  4. UTF-16 使用二或四个字节为每个字符编码
  5. UTF-32 使用四个字节为每个字符编码
  6. 一串中文字符通过UTF-8进行编码传输给别人,别人拿到这串文字之后,通过GBK进行解码,那么格式就会出现乱码。

泛型

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

  1. 适用于多种数据类型执行相同的代码(代码复用)
  2. 泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型)
  3. 在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制

上限:

class Info<T extends Number>{    // 此处泛型只能是数字类型
    private T var ;        // 定义泛型变量
    public void setVar(T var){
        this.var = var ;
    }
    public T getVar(){
        return this.var ;
    }
    public String toString(){    // 直接打印
        return this.var.toString() ;
    }
}
public class demo1{
    public static void main(String args[]){
        Info<Integer> i1 = new Info<Integer>() ;        // 声明Integer的泛型对象
    }
}

下限:

class Info<T>{
    private T var ;        // 定义泛型变量
    public void setVar(T var){
        this.var = var ;
    }
    public T getVar(){
        return this.var ;
    }
    public String toString(){    // 直接打印
        return this.var.toString() ;
    }
}
public class GenericsDemo21{
    public static void main(String args[]){
        Info<String> i1 = new Info<String>() ;        // 声明String的泛型对象
        Info<Object> i2 = new Info<Object>() ;        // 声明Object的泛型对象
        i1.setVar("hello") ;
        i2.setVar(new Object()) ;
        fun(i1) ;
        fun(i2) ;
    }
    public static void fun(Info<? super String> temp){    // 只能接收String或Object类型的泛型,String类的父类只有Object类
        System.out.print(temp + ", ") ;
    }
}

总结:

  • <? extends T> 表示类型的上界,表示参数化类型的可能是T 或是 T的子类
  • <? super T> 表示类型下界(Java Core中叫超类型限定),表示参数化类型是此类型的超类型(父类型),直至Object

如何定义这个上限下限?
遵守PECS原则,即Producer Extends, Consumer Super;上界生产,下界消费。

  • 生产者(Producer):集合向外提供数据(只读操作,如 get())。
  • 消费者(Consumer):集合接收外部写入数据(只写操作,如 add())。
需求 通配符选择 原因
从集合读取数据 ? extends T 确保返回类型是T或其子类
向集合写入数据 ? super T 确保写入类型是T或其父类
既读又写 不使用通配符 必须指定具体类型(如T
// 只能从集合中读取数据(生产者)
public void processNumbers(List<? extends Number> numbers) {
    Number num = numbers.get(0); // 安全:返回值一定是Number或其子类
    // numbers.add(10); // 编译错误!无法确定集合实际类型
}


// 只能向集合中写入数据(消费者)
public void addElements(List<? super Integer> list) {
    list.add(10);      // 安全:Integer可以存入Integer或其父类
    // Integer x = list.get(0); // 编译错误!返回类型是Object
}

泛型中K T V E ? Object等分别代表什么含义。
E – Element (在集合中使用,因为集合中存放的是元素)

T – Type(Java 类)

K – Key(键)

V – Value(值)

N – Number(数值类型)

? – 表示不确定的java类型(无限制通配符类型)

S、U、V – 这几个有时候也有,这些字母本身没有特定的含义,它们只是代表某种未指定的类型。一般认为和T差不多。

Object – 是所有类的根类,任何类的对象都可以设置给该Object引用变量,使用的时候可能需要类型强制转换,但是用使用了泛型T、E等这些标识符后,在实际用之前类型就已经确定了,不需要再进行类型强制转换。

类型擦除【编译阶段】
Java中的泛型通过类型擦除的方式来实现,通俗点理解,就是通过语法糖的形式,在.java->.class转换的阶段,将List擦除调转为List的手段。换句话说,Java的泛型只在编译期,Jvm是不会感知到泛型的。

List<?>, List<Object>, List之间的区别

  1. List<?> 是一个未知类型的List,而List<Object> 其实是任意类型的List。可以把List<String>, List<Integer>赋值给List<?>,却不能把List<String>赋值给 List<Object>
  2. 可以把任何带参数的类型传递给原始类型List,但却不能把List<String>赋值给List<Object>,因为会产生编译错误
  3. List<?>由于不确定列表中元素的具体类型,因此只能从这种列表中读取数据,而不能往里面添加除了 null 之外的任何元素。

在这里插入图片描述
在这里插入图片描述


接口和抽象类的区别

  1. 接口只是定义了一些方法而已,在不考虑Java8中default方法情况下,接口中只有抽象方法,是没有实现的代码的。(Java8中可以有默认方法和静态方法)
  2. 抽象类中的修饰符可以有public、protected和private和这些修饰符,而接口中默认修饰符是public。不可以使用其它修饰符。(接口中,如果定义了成员变量,还必须要初始化)
  3. 抽象类可以有构造器。接口不能有构造器。【抽象类不能直接被实例化(new)出来,但是构造器也是有意义的,能起到初始化共有成员变量、强制初始化操作等作用。】
  4. 抽象类 单继承, 接口 多实现,同时接口支持多重继承,一个接口可以继承多个其他接口。
  5. 接口和抽象类的职责不一样。接口主要用于制定规范,因为我们提倡也经常使用的都是面向接口编程。而抽象类主要目的是为了复用,比较典型的就是模板方法模式。

面向对象和面向过程

  1. 面向过程把问题分解成一个一个步骤,每个步骤用函数实现,依次调用即可。
  2. 面向对象将问题分解成一个一个步骤,对每个步骤进行相应的抽象,形成对象,通过不同对象之间的调用,组合解决问题。

三大基本特征:封装、继承、多态。

  • 封装:数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。
  • 继承:继承的主要目的就是为了复用。子类可以继承父类,这样就可以把父类的属性和方法继承过来。
  • 多态:就是同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。 编译时多态主要指方法的重载。运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定(重写)。

面向对象的五大基本原则:

  • 单一职责原则(Single-Responsibility Principle
    • 内容:一个类最好只做一件事
    • 提高可维护性:当一个类只负责一个功能时,其实现通常更简单、更直接,这使得理解和维护变得更容易。
    • 减少代码修改的影响:更改影响较小的部分,因此减少了对系统其他部分的潜在破坏。
  • 开放封闭原则(Open-Closed principle)
    • 内容:对扩展开放、对修改封闭
    • 促进可扩展性:可以在不修改现有代码的情况下扩展功能,这意味着新的功能可以添加,而不会影响旧的功能。
    • 降低风险:由于不需要修改现有代码,因此引入新错误的风险较低。
  • Liskov替换原则(Liskov-Substituion Principle)
    • 内容:子类必须能够替换其基类
    • 提高代码的可互换性:能够用派生类的实例替换基类的实例,使得代码更加模块化,提高了其灵活性。
    • 增加代码的可重用性:遵循LSP的类和组件更容易被重用于不同的上下文。
  • 依赖倒置原则(Dependency-Inversion Principle)
    • 内容:程序要依赖于抽象接口,而不是具体的实现
    • 提高代码的可测试性:通过依赖于抽象而不是具体实现,可以轻松地对代码进行单元测试。
    • 减少系统耦合:系统的高层模块不依赖于低层模块的具体实现,从而使得系统更加灵活和可维护。
  • 接口隔离原则(Interface-Segregation Principle)。
    • 内容:使用多个小的专门的接口,而不要使用一个大的总接口
    • 减少系统耦合:通过使用专门的接口而不是一个大而全的接口,系统中的不同部分之间的依赖性减少了。
    • 提升灵活性和稳定性:更改一个小接口比更改一个大接口风险更低,更容易管理。

反射机制

反射机制指的是程序在运行时能够获取自身的信息。在java中,只要给定类的名字,那么就可以通过反射机制来获得类的所有属性和方法。

反射的好处就是可以提升程序的灵活性和扩展性,比较容易在运行期干很多事情。但是他带来的问题更多,主要由以下几个:

1、代码可读性低及可维护性

2、反射代码执行的性能低

3、反射破坏了封装性

那么,反射为什么慢呢?主要由以下几个原因:

1、由于反射涉及动态解析的类型,因此不能执行某些Java虚拟机优化,如JIT优化。

2、在使用反射时,参数需要包装(boxing)成Object[] 类型,但是真正方法执行的时候,又需要再拆包(unboxing)成真正的类型,这些动作不仅消耗时间,而且过程中也会产生很多对象,对象一多就容易导致GC,GC也会导致应用变慢。

3、反射调用方法时会从方法数组中遍历查找,并且会检查可见性。这些动作都是耗时的。

4、不仅方法的可见性要做检查,参数也需要做很多额外的检查。

反射的运用场景:

  1. 动态代理
  2. JDBC的class.forName
  3. BeanUtils中属性值的拷贝
  4. RPC框架
  5. ORM框架
  6. Spring的IOC/DI

Java的Class类是java反射机制的基础,通过Class类我们可以获得关于一个类的相关信息。

在类加载的时候,jvm会创建一个class对象class对象是可以说是反射中最常用的,获取class对象的方式的主要有三种

  • 根据类名:类名.class
  • 根据对象:对象.getClass()
  • 根据全限定类名:Class.forName(全限定类名)

Java.lang.Class是一个比较特殊的类,它用于封装被装入到JVM中的类(包括类和接口)的信息。当一个类或接口被装入到JVM时便会产生一个与之关联的java.lang.Class对象,可以通过这个Class对象对被装入类的详细信息进行访问。

虚拟机为每种类型管理一个独一无二的Class对象。也就是说,每个类(型)都有一个Class对象。运行程序时,Java虚拟机(JVM)首先检查是否所要加载的类对应的Class对象是否已经加载。如果没有加载,JVM就会根据类名查找.class文件,并将其Class对象载入。


深拷贝和浅拷贝,序列化和反序列化

仅复制对象本身,对象内部的引用类型还是原来的引用地址——浅拷贝。
即复制对象本身,又复制对象内部的引用类型——深拷贝。

在Object类中定义了一个clone方法,这个方法其实在不重写的情况下,其实也是浅拷贝的。

如果想要实现深拷贝,就需要重写clone方法,而想要重写clone方法,就必须实现Cloneable,否则会报CloneNotSupportedException异常。

重写clone方法 需要对内部的引用对象 遍历 重新赋值,这样有点繁琐,还可以借助序列化来实现深拷贝。先把对象序列化成流,再从流中反序列化成对象。


Java 序列化是一种将对象转换为字节流的过程,以便可以将对象保存到磁盘上,将其传输到网络上,或者将其存储在内存中,以后再进行反序列化,将字节流重新转换为对象。

序列化在 Java 中是通过 java.io.Serializable 接口来实现的,该接口没有任何方法,只是一个标记接口,用于标识类可以被序列化。

以下几个和序列化&反序列化有关的知识点大家可以重点关注一下:

1、在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化。

2、通过ObjectOutputStream和ObjectInputStream对对象进行序列化及反序列化

3、虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)

4、序列化并不保存静态变量。

5、要想将父类对象也序列化,就需要让父类也实现Serializable 接口

6、transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

7、服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。


AIO、BIO和NIO

BIO (Blocking I/O):同步阻塞I/O,是JDK1.4之前的传统IO模型。 线程发起IO请求后,一直阻塞,直到缓冲区数据就绪后,再进入下一步操作。

NIO (Non-Blocking I/O):同步非阻塞IO,线程发起IO请求后,不需要阻塞,立即返回。用户线程不原地等待IO缓冲区,可以先做一些其他操作,只需要定时轮询检查IO缓冲区数据是否就绪即可。

AIO ( Asynchronous I/O):异步非阻塞I/O模型。线程发起IO请求后,不需要阻塞,立即返回,也不需要定时轮询检查结果,异步IO操作之后会回调通知调用方。

BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。

NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。

AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。


SPI

SPI(Service Provider Interface) 是 Java 提供的一种服务发现机制:运行时动态加载实现类。

适用于:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略。比较常见的例子:

  1. 数据库驱动加载接口实现类的加载
  2. JDBC加载不同类型数据库的驱动
  3. 日志门面接口实现类加载
  4. SLF4J加载不同提供商的日志实现类

BigDecimal

  1. equals和compareTo
    BigDecimal的equals方法和compareTo并不一样,equals方法会比较两部分内容,分别是值(value)和标度(scale),而对于0.1和0.10这两个数字,他们的值虽然一样,但是精度是不一样的,所以在使用equals比较的时候会返回false。

  2. 示例

public class Main {
    public static void main(String[] args) {
        BigDecimal bigDecimal = new BigDecimal(1);
        BigDecimal bigDecimal1 = new BigDecimal(1);
        System.out.println(bigDecimal.equals(bigDecimal1));// true


        BigDecimal bigDecimal2 = new BigDecimal(1);
        BigDecimal bigDecimal3 = new BigDecimal(1.0);
        System.out.println(bigDecimal2.equals(bigDecimal3));// true


        BigDecimal bigDecimal4 = new BigDecimal("1");
        BigDecimal bigDecimal5 = new BigDecimal("1.0");
        System.out.println(bigDecimal4.equals(bigDecimal5)); // false
    }

}

new BigDecimal(double) 中如果 double 是 精确的整数(比如 1.0),则构造出来的 BigDecimal 值和 scale 都是和 new BigDecimal(1) 一致的。


Java动态代理

在Java中,实现动态代理有两种方式:

  1. JDK动态代理:Java.lang.reflect 包中的Proxy类和InvocationHandler接口提供了生成动态代理类的能力。基于接口
  2. Cglib动态代理:Cglib (Code Generation Library )是一个第三方代码生成类库,运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展。基于子类

JDK的动态代理有一个限制,就是使用动态代理的对象必须实现一个或多个接口

Cglib是一个强大的高性能的代码生成包,它可以在运行期扩展Java类与实现Java接口。它广泛的被许多AOP的框架使用,例如Spring AOP和dynaop,为他们提供方法的interception(拦截)。

Cglib包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。

所以,使用JDK动态代理的对象必须实现一个或多个接口;而使用cglib代理的对象则无需实现接口,达到代理类无侵入。

Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理。

  • JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。
  • InvocationHandler 接口:这是代理实例的调用处理程序实现的接口,它只有一个invoke方法,所有对代理对象的方法调用都会被转发到这个方法。
  • 如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。
  • CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

Java是值传递还是引用传递

值传递,只不过对于Java对象的传递,传递的内容是对象的引用。

Java 统一是值传递。对于对象参数,传递的是对象引用的副本,也就是说是对象在内存中的地址的一份副本。方法内可以通过这个引用修改对象的状态,但重新赋值引用不会影响原始对象的引用。


Java中创建对象有哪些种方式

  1. 使用new关键字

  2. 使用反射机制
    调用Java.lang.Class或者java.lang.reflect.Constructor类的newInstance()实例方法:

User user = (User)Class.forName("xxx.xxx.User").newInstance(); 
User user = User.class.newInstance();

Constructor  constructor = User.class.getConstructor();
User user = constructor.newInstance();
  1. 使用clone方法【要使用clone方法,我们需要先实现Cloneable接口并实现其定义的clone方法。如果只实现了Cloneable接口,并没有重写clone方法的话,会默认使用Object类中的clone方法,这是一个native的方法。】
  2. 使用反序列化
    当我们序列化和反序列化一个对象,jvm会给我们创建一个单独的对象。其实反序列化也是基于反射实现的。

String

  1. intern() 方法
String s3 = new String("a") + new String("a");

等价于:【JDK9后做了优化】

String s3 = new StringBuilder()
    .append(new String("a"))
    .append(new String("a"))
    .toString();

intern() 方法会尝试将当前字符串对象加入到 JVM 的 字符串常量池(String Pool) 中,如果池中已经存在一个 内容相同的字符串,就返回池中那个字符串的引用;否则就把当前字符串的引用加入池中,并返回这个引用。
如果常量池中不存在当前字符串,则把这个对象的引用(堆中的那个对象)直接放入常量池。
所以下面 是true不是复制,而是直接把 s3 的引用放入常量池!

        String s3 = new String("a") + new String("a");
        s3.intern();
        String s4 = "aa";
        System.out.println(s3 == s4);// true

但是如果已经存在于常量池中:

String s4 = "aa"; // ① 先出现字面量
String s3 = new String("a") + new String("a");
s3.intern();
System.out.println(s3 == s4); // false!
  • 常量池中已经有 “aa”(由字面量创建),

  • s3.intern() 不会再放 s3 进去,而是返回已有的池中对象。

  • 所以 s3 和 s4 是不同的对象。


  1. == 地址判断
        String s = "ab";
        String t = "a"+"b";
        System.out.println(s == t); // true

        String a = "a";
        String b = "b";
        String s1 = a + b;
        System.out.println(s1 == s); // false
表达式 发生时间 是否常量池引用 备注
"a" + "b" 编译期优化 ✅ 是 常量折叠
"a" + b(b是变量) 运行期拼接 ❌ 否 StringBuilder
new String("ab") 明确新对象 ❌ 否 在堆上
"ab" 编译期常量 ✅ 是 字面量放入常量池

  1. String str=new String("abc") 会创建了几个对象

创建的对象数应该是1个或者2个。

  • 一次new的过程,都会在堆上创建一个对象。
  • 如果常量池里面没有这个引用,那么就会在常量池中创建1个,共2个。如果常量池已经存在,那么就不会创建了。