Java面试题基础

基础总结


Java

Java 平台无关性

主要通过三个方面实现.

  • Java 语言规范: 通过规定 Java 语言中基本数据类型的取值范围和行为,比如 int 长度为 4 字节,这是固定的。
  • Class 文件: 所有 Java 文件要通过 javac 或者其他一些 java 编译器编译成统一的 Class 文件。
  • Java 虚拟机:
    • 通过 Java 虚拟机将字节码文件 (.Class) 转成对应平台的二进制文件。
    • JVM 是平台相关的,需要在不同操作系统安装对应的虚拟机。

程序运行过程

  • 编译:java 源文件编译为 class 字节码文件
  • 类加载:类加载器把字节码加载到虚拟机的方法区。
  • 创建对象: 运行时创建对象
  • 方法调用: JVM 执行引擎解释为机器码
  • 执行: CPU 执行指令
  • running: 多线程切换上下文

Java 三大特性

  • 封装:将一系列的操作和数据组合在一个包中,使用者调用这个包的时候不必了解包中具体的方法是如何实现的。
  • 多态 :父类的变量可以引用一个子类的对象,在运行时通过动态绑定来决定调用方法。
    作用:
    • 应用程序不必为每一个派生类编写功能调用,只需要对抽象基类进行处理即可。大大提高程 序的可复用性。// 继承
    • Java的多态是指同一个方法在不同对象中可以表现出不同的行为。多态分为编译时多态(方法重载)和运行时多态(方法重写)。
    • Java接口的实现也是多态的一种表现形式。通过接口,多个类可以实现相同的方法,从而在运行时表现出不同的行为。
    • 派生类的功能可以被基类的方法或引用变量所调用,这叫向后兼容,可以提高可扩充性和可维护性。
  • 继承:一个类可以扩展出一个子类,子类可以继承父类的属性和方法,也可以添加自己的成员变量和方法。接口可以多继承,普通类只能单继承。

重载和重写(针对于方法)

  • 重写 :子类具有和父类方法名和参数列表都相同的方法,返回值要不大于父类方法的返回值,抛出的异常要不大于父类抛出的异常,方法修饰符可见性要不小于父类。运行时多态。
    • 是运行时多态,因为程序运行时,会从调用方法的类中根据继承关系逐级往上寻找该方法,这是在运行时才能进行的。
  • 重载:同一个类中具有方法名相同但参数列表不同的方法,返回值不做要求。编译时多态。

Integer 和 int 区别

  • Integer 是 int 的包装类,所表示的变量是一个对象;而 int 所表示的变量是基本数据类型
  • 自动装箱 (valueOf) 指的是将基本数据类型包装为一个包装类对象,自动拆箱 (intValue) 指的是将一个包装类对象转换为一个基本数据类型。
  • 包装类的比较使用 equals,是对象间的比较。

基本数据类型(8种)

  • byte 1 字节;short 2 字节
  • int, float 4 字节
  • long, double 8 字节
  • boolean 单独出现时 4 字节,数组时单个元素 1 字节
  • char 英文都是 1 字节,GBK 中文 2 字节,UTF-8 中文 3 字节

值传递和引用传递

  • 值传递对基本数据类型而言的,传递的是变量值的一个副本,改变副本不影响原变量的值
  • 引用传递对于对象型变量而言,传递的是对象地址的副本,不是原变量本身,所以对引用对象的操作会改变原变量的值。

== 和 equals 区别

  • == 比较的对象如果是基本数据类型,就是两者的值进行比较;如果是引用对象的比较,是判断对象的地址值是否相同
  • equals 如果比较的是 String 对象,就是判断字符串的值是否相同;如果比较的是 Object 对象,比较的是引用的地址内存;可以通过重写 equals 方法来自定义比较规则,也需要同时重写 hashCode 方法

方法修饰符可见类型

  • public: 对本包和不同包都是可见的
  • protected: 对不同包不可见
  • default: 只对本包中子类和本类可见
  • private:只对本类可见

Object 类,equals 和 hashCode

Object 的类是所有类的父类。equals,hashCode,toString 方法

  • equals 用来比较对象地址值是否相同
  • hashCode 返回由对象地址计算得出的一个哈希值
  • 两者要同时重写的原因
    • 使用 hashcode 方法提前校验,通过 hasCode 比较比较快,可以避免每一次比对都调用 equals 方法,提高效率
    • 保证是同一个对象,如果重写了 equals 方法,而没有重写 hashCode 方法,会出现 equals 比较时相等的对象,hashCode 不相等的情况,重写 hashcode 方法就是为了避免这种情况的出现。
  • 哈希值相同的对象 equals 比较不一定相等,存在两个对象计算得到 hashCode 相等的情况,这是哈希冲突。

避免哈希冲突?

  • 哈希表的特点:关键字在表中位置和它之间存在一种确定的关系。
  • 解决哈希冲突:
    • 开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
      • 线性探测再散列:放入元素,如果发生冲突,就往后找没有元素的位置;
      • 平方探测再散列:如果发生冲突,放到 (冲突 + 1 平方) 的位置,如果还发生冲突,就放到 (冲突 - 1 平方) 的位置;如果还有人就放到 (冲突 + 2 平方) 的位置,以此类推,要是负数就倒序数。
      • 随机探测再散列
    • 链地址法:如果发生冲突,就继续往前一个元素上链接,形成一个链表,Java 的 hashmap 就是这种方法。
    • 再哈希:用另一个方法再次进行一个哈希运算
    • 建立一个公共溢出区:将哈希表分为基本表和溢出表两部分,范式和基本表发生冲突的元素,一律填入溢出表。

深拷贝,浅拷贝

clone:clone 方法声明为 protected,类只能通过该方法克隆它自己的对象,如果希望其他类也能调用该方法必须定义该方法为 public。如果一个对象的类没有实现 Cloneable 接口,该对象调用 clone 方法会抛出一个 CloneNotSupport 异常。默认的 clone 方法是浅拷贝,一般重写 clone 方法需要实现 Cloneable 接口并指定访问修饰符为 public。

  • 浅拷贝:重新在堆中创建内存,将对象进行拷贝,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因为共享同一块内存,会相互影响。(被浅拷贝的对象是会重新生成一个新的对象,新的对象和原来的对象是没有任何关系的,)如果对象中的某个属性是引用类型的话,那么该属性对应的对象是不会重新生成的,浅拷贝只会重新当前拷贝的对象,并不会重新生成其属性引用的对象。

  • 深拷贝:从堆内存中开辟一个新的区域存放新对象,会把拷贝的对象和其属性引用的对象都重新生成新的对象。

    • 实现:对拷贝的对象中所引用的数据类型再进行以拷贝;使用序列化
      • 序列化实现样例
java 复制代码
import java.io.*;

class Person implements Serializable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Getter和Setter
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + '}';
    }

    // 深拷贝方法
    public Person deepClone() {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(this);
            oos.flush();
            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            return (Person) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }
}

public class DeepCopyExample {
    public static void main(String[] args) {
        Person original = new Person("Alice", 30);
        Person copy = original.deepClone();

        System.out.println("Original: " + original);
        System.out.println("Copy: " + copy);
    }
}

内部类

使用内部类主要有两个原因:内部类可以对同一个包中的其他类隐藏。内部类方法可以访问定义这个内部类的作用域中的数据,包括原本私有的数据。内部类是一个编译器现象,与虚拟机无关。编译器会把内部类转换成常规的类文件,用美元符号 $ 分隔外部类名与内部类名,而虚拟机对此一无所知。

  • 静态内部类:由 static 修饰,属于外部类本身,只加载一次。类可以定义的成分静态内部类都可以定义,可以访问外部类的静态变量和方法,通过 new 外部类.内部类构造器 来创建对象。只要内部类不需要访问外部类对象,就应该使用静态内部类。
  • 成员内部类:属于外部类的每个对象,随对象一起加载。不可以定义静态成员和方法,可以访问外部类的所有内容,通过 new 外部类构造器.new 内部类构造器 来创建对象。
  • 局部内部类:定义在方法、构造器、代码块、循环中。不能声明访问修饰符,只能定义实例成员变量和实例方法,作用范围仅在声明这个局部类的代码块中。
  • 匿名内部类:没有名字的局部内部类,可以简化代码,匿名内部类会立即创建一个匿名内部类的对象返回,对象类型相当于当前 new 的类的子类类型。匿名内部类一般用于实现事件监听器和其他回调。

类初始化的顺序

  • 父类静态变量和静态代码块
  • 子类静态变量和静态代码块
  • 父类普通变量和代码块
  • 父类构造器
  • 子类普通变量和代码块
  • 子类构造器

String,StringBuilder,StringBuffer

String
  • 一旦被创建就不可被修改,所以修改 String 变量值的时候是新建了一个 String 对象,赋值给 原变量引用
  • 两种创建方法
    • 直接赋值一个字符串,就是将字符串放进常量池,位于栈中的变量直接引用常量池中的字符串。
    • new 方式创建先在堆中创建 String 对象,再去常量池中查找是否有赋值的字符串常量,找到了就直接使用,没找到就开辟空间存字符串。通过变量引用对象,对象引用字符串的形式创建。
StringBuilder & StringBuffer
  • 都继承自 AbstractStringBuilder 类,是可变类(这是加分项)
  • 前者线程不安全,后者通过 synchronized 锁保证线程安全
  • 因此 StringBuilder 执行效率高,StringBuffer 执行效率低

final 关键字

  • 所修饰的变量,是基本数据类型则值不能改变,访问时会被当做一个常量;是引用型变量的话,初始化后就不能指向另一个对象了。而且一定要显示地初始化赋值。
  • 所修饰的类,不能被继承,其中方法默认是 final 修饰
  • final 修饰的方法不可被重写,但可以被重载
  • final 关键字与变量的作用域有关,而不是直接与类或对象绑定。具体来说:
bash 复制代码
final 变量:
每个对象都有自己的 final 变量。如果你在类中定义了一个 final 实例变量,每个对象都会有自己的副本,并且这些副本在对象创建后不能被修改。
final 类:
如果一个类被声明为 final,则不能被继承。这是类的特性,而不是对象的特性。
final 方法:
如果一个方法被声明为 final,则不能被子类重写。

static 关键字

  • 修饰代码块,使这个代码块在 JVM 加载之处就开辟一块空间单独存放代码块内容,且只加载一次。执行得到的结果存储在方法区并被线程共享。静态类中的方法直接和这个类关联,而不是和这个对象关联。可以直接通过类名来使用方法。
  • 修饰非局部的成员变量,加载方式和静态代码块一样。由于在 JVM 内存中共享,会引起线程安全问题。解决:加 final;使用同步(volatile 关键字)。如下
java 复制代码
public class Counter {
    // 静态变量
    static int count = 0;

    // 构造函数
    public Counter() {
        count++; // 每次创建实例时,count自增
    }

    // 静态方法
    public static void displayCount() {
        System.out.println("当前计数: " + count);
    }

    public static void main(String[] args) {
        Counter c1 = new Counter();
        Counter c2 = new Counter();
        Counter c3 = new Counter();

        // 显示计数
        Counter.displayCount(); // 输出: 当前计数: 3
    }
}
  • 修饰方法,通过类名调用。静态方法不可以直接调用其他成员方法、成员变量。

抽象类和接口

接口只能用 *public * 和 abstract * 修饰
区别分为四个方面:

  • 成员变量:接口中默认 public static final
  • 成员方法:java8 之前接口中默认是 public,java8 加入了 static 和 default,java9 中加入了 private,方法不能用 final 修饰,因为需要实现类重写;抽象类无限制
  • 构造器:接口和抽象类都不能被实例化,但接口中没有构造器,抽象类中有
  • 继承:接口可以多继承,抽象类只能单继承
抽象类和接口的选择?

如果知道某个类应该成为基类,那么第一选择应该是让它成为一个接口,只有在必须要有方法定义和成员变量的时候,才应该选择抽象类。在接口和抽象类的选择上,必须遵守这样一个原则:行为模型应该总是通过接口而不是抽象类定义。通过抽象类建立行为模型会出现的问题:如果有一个产品类 A,有两个子类 B 和 C 分别有自己的功能,如果出现一个既有 B 产品功能又有 C 产品功能的新产品需求,由于 Java 不允许多继承就出现了问题,而如果是接口的话只需要同时实现两个接口即可。

异常

所有的异常都继承自 Throwable 类的,分为 Error 和 Exception。

  • Error 类描述了 Java 运行时系统的内部错误和资源耗尽错误,如果出现了这种错误,一般无能为力。
  • Error 和 RuntimeException 的异常属于非检查型异常,其他的都是检查型异常。

常见的 RuntimeException 异常:

  • ClassCastException,错误的强制类型转换。

  • ArrayIndexOutOfBoundsException,数组访问越界。

  • NullPointerException,空指针异常。

常见的检查型异常:

  • FileNotFoundException,试图打开不存在的文件。

  • ClassNotFoundException,试图根据指定字符串查找 Class 对象,而这个类并不存在。

  • IOException,试图超越文件末尾继续读取数据。

异常处理:

  • 抛出异常:遇到异常不进行具体处理,而是将异常抛出给调用者,由调用者根据情况处理。抛出异常有 2 种形式,一种是 throws 关键字声明抛出的异常,作用在方法上,一种是使用 throw 语句直接抛出异常,作用在方法内。
  • 捕获异常:使用 try/catch 进行异常的捕获,try 中发生的异常会被 catch 代码块捕获,根据情况进行处理,如果有 finally 代码块无论是否发生异常都会执行,一般用于释放资源,Java 7 开始可以将资源定义在 try 代码块中自动释放资源。
  • try-catch-finally
    • finally 对 try 块中打开的物理资源进行回收 (JVM 垃圾回收机制回收对象占用的内存)。
    • 这个回收如果放在 catch 中执行,不发生异常则不会被执行;放在 try 中,如发生异常前就被回收,那么 catch 就不会被执行。
    • java7 可以在 try () 圆括号中初始化或声明资源,会自动回收。但资源需要实现 AutoCloseable 接口

序列化

Java 对象在 JVM 运行时被创建,JVM 退出时存活对象被销毁。为了保证对象及其状态的持久化,就需要使用序列化了。序列化就是将对象通过 ObjectOutputStream 保存为字节流;反序列化就是将字节流还原为对象。

  • 要实现 Serializable 接口来进行序列化。
  • 序列化和反序列化必须保持序列化 ID 的一致。
  • 静态、transient 修饰的变量和方法不能被序列化。
  • 实现 Externalizable 可以自行决定哪些属性可以被序列化

反射

在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取信息以及动态调用对象方法的功能就是 Java 的反射机制。优点是运行时动态获取类的全部信息,缺点是破坏了类的封装性,泛型的约束性。

  • Class 类保存对象运行时信息,可以通过①类名.class ②对象名.getClass ()③Class.forName (类的全限定名) 方式获取 Class 实例
  • Class 类中的 getFields () 返回这个类支持的公共字段;getMethods () 返回公共方法;- getCosntructors () 返回构造器数组(包括父类公共成员)
  • xxxDeclaredxxx () 可以返回全部字段、方法和构造器的数组(不包括父类的成员)

反射是Java语言本身提供的特性,不是Spring框架做的。Java的反射机制允许程序在运行时检查类的属性、方法和构造函数,并可以动态地调用它们。
Spring框架利用了Java的反射机制来实现许多功能,比如依赖注入和AOP(面向切面编程)。但反射的基础功能是Java语言自带的。

IOC控制反转

在Java中,IOC(控制反转)是一种设计原则,主要用于实现对象之间的解耦。IOC的核心思想是将对象的创建和管理交给容器,而不是由对象自身控制。这种方式使得对象的依赖关系更加灵活,便于测试和维护。

主要概念

  • 依赖注入(DI): IOC的常用实现方式,通过构造函数、属性或方法注入依赖对象。
  • 容器: 管理对象生命周期和依赖关系的框架,如Spring框架。
  • 解耦: 通过将对象的依赖关系移交给外部容器,从而减少类之间的耦合度。

优点

  • 可测试性: 方便进行单元测试,因为可以轻松替换依赖。
  • 灵活性: 可以根据需要更改依赖实现,而不需要修改使用它们的代码。
  • 维护性: 代码清晰,逻辑分离,易于维护和扩展。

示例

在Spring中,使用@Autowired注解进行依赖注入:

java 复制代码
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    // 业务逻辑方法
}

在这个示例中,UserRepository的实例由Spring容器管理,UserService不需要自己创建或管理它。

注解

可以给类、接口或者方法、变量添加一些额外信息;帮助编译器和 JVM 完成一些特定功能。

  • 元注解:我们可以自定义一个注解,这时就需要在自定义注解中使用元注解来标识一些信息
    • @Target:约束注解作用位置:METHOD,VARIABLE,TYPE,PARAMETER,CONSTRUCTORS,LOACL_VARIABLE
    • @Rentention:约束注解作用的生命周期:SOURCE 源码,CLASS 字节码,RUNTIME 运行时
    • @Documented:表明这个注解应该被 javadoc 工具记录
    • @Inherited:表面某个被标注的类型是被继承

Java中的注解是Java语言自带的特性,首次引入于Java 5。它们允许开发者在代码中添加元数据,用于配置和处理。
Spring框架则使用了Java注解来提供一些功能,例如:
@Autowired 用于自动装配依赖。
@Component 指示一个类是Spring的组件。
总的来说,注解是Java自带的,而Spring框架只是利用了这些注解来实现其功能。

泛型

泛型的提出是为了编写重用性更好的代码。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

那么 Java 之所以引入它我认为主要有三个作用

  • 类型检查,它将运行时类型转换的 ClassCastException 通过泛型提前到编译时期。
    避免类型强转。
  • 泛型可以泛型算法,增加代码的复用性。
    实现
  • 泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如 List 在运行时仅用一个 List 来表示。这样做的目的,是确保能和 Java 5 之前的版本开发二进制类库进行兼容。

Java 的泛型是如何工作的?什么是类型擦除?如何工作?

1、类型检查:在生成字节码之前提供类型检查

2、类型擦除:所有类型参数都用他们的限定类型替换,包括类、变量和方法(类型擦除)

3、如果类型擦除和多态性发生了冲突时,则在子类中生成桥方法解决

4、如果调用泛型方法的返回类型被擦除,则在调用该方法时插入强制类型转换


数据结构

Java数据结构


JVM

JVM八股


并发编程

并发编程


Java框架

框架

相关推荐
阑梦清川3 小时前
在鱼皮的模拟面试里面学习有感
学习·面试·职场和发展
鱼跃鹰飞12 小时前
大厂面试真题-简单说说线程池接到新任务之后的操作流程
java·jvm·面试
程序员清风15 小时前
浅析Web实时通信技术!
java·后端·面试
测试199815 小时前
外包干了2年,快要废了。。。
自动化测试·软件测试·python·面试·职场和发展·单元测试·压力测试
mingzhi6116 小时前
渗透测试-快速获取目标中存在的漏洞(小白版)
安全·web安全·面试·职场和发展
嚣张农民17 小时前
一文简单看懂Promise实现原理
前端·javascript·面试
Liknana18 小时前
Android 网易游戏面经
android·面试
威哥爱编程1 天前
MongoDB面试专题33道解析
数据库·mongodb·面试
程序猿进阶1 天前
Redis 基础数据改造
java·开发语言·数据库·redis·后端·面试·架构