面试题专题一: Java基础

本文深入探讨Java基础知识,包括数据类型、字符串、集合类、线程安全、类加载机制、IO模型、反射、动态代理、单例模式、访问修饰符、序列化、泛型以及HashSet的内部工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

面试题专题一

Java基础

1. JAVA 中的几种基本数据类型是什么,各自占用多少字节

int :4

long:8

float:4

double:8

boolean:2

char:2

byte:1

short:2

2. String 类能被继承吗,为什么。

不能。String类底层是一个由final关键字修饰的value数组,由final修饰的变量表示不可改变。

同时Stirng类也是由final修饰的,不可被继承。

3. String,StringBuffer,StringBuilder 的区别

可变性:

String 不可变,StringBuffer,StringBuilder可变

线程安全性:

String不可变,线程安全

StringBuilder 继承AbstractStringBuilder类,没有final修饰,没有加锁,线程不安全

StringBuffer继承AbstractStringBuilder类,方法有加syncronized锁,线程安全

4. ArrayList 和 LinkedList 有什么区别。

底层数据结构

ArrayList底层是由数组构成的

LinkedList底层是由双向循环链表构成的

扩容机制

ArrayList初始容量为10。这里注意,当我们第一次new一个空的ArrayList时,容量是为0,当我们第一次add一个值时,容量才初始化为10。容量不足时扩容为原来的 1.5 倍。底层调用Arrays.copyOf方法将原数组中数据复制到新的数组中,因此扩容效率低。

LinkedList由于基于链表实现,因此没有初始容量,没有扩容机制。

操作效率

ArrayList 基于数组实现,因此可实现O(1)随机取值,但是插入和删除效率较低。

LinkedList基于链表实现,查询效率较低,但是插入和删除指定结点效率高。

线程安全

二者都是非线程安全的。

5. 讲讲类的实例化顺序,比如父类静态数据,构造函数,字段,子类静态数据,构造函数,字段,当 new 的时候,他们的执行顺序。

说到类的实例化,就需要讲一下JVM中类的生命周期。

一个类经历了加载、连接、初始化、使用、销毁这五个阶段。在连接中的准备阶段,JVM会为类的静态变量分配内存,然后初始化为默认值。因此静态数据一定是优先于普通数据进行实例化

根据 JVM 的类初始化机制,假如某个类存在直接父类,那么就会优先初始化父类,再初始化子类。剩下的语句按照代码中从上到下的顺序依次执行。

所以结论就是:

  • 父类静态变量
  • 父类静态代码块
  • 子类静态变量
  • 子类静态代码块
  • 父类普通变量
  • 父类普通代码块
  • 父类构造函数
  • 子类普通变量
  • 子类普通代码块
  • 子类构造函数

6. Map相关

6.1 用过哪些 Map类

使用过继承了AbstractMap接口的HashMapConcurrentHashMapTreeMap

6.2 它们的区别

底层数据结构

HashMapConcurrentHashMap: 数组+链表+红黑树

TreeMap:红黑树

排序

HashMapConcurrentHashMap 内部元素无序排列

TreeMap内部元素按照插入的Key值进行排序。

元素限制

HashMap允许 keyvalue都为空

ConcurrentHashMapTreeMap不允许 keyvalue任意一个为空

实现的接口关系

HashMap直接实现Map接口

ConcurrentHashMap —> ConcurrentMap —> Map

TreeMap—> NavigableMap —> SortedMap —> Map

线程安全

HashMap线程不安全,会出现fast-fail错误。jdk1.7,HashMap在扩容时会导致死循环,主要原因是插入链表采用的是头插法。 Jdk1.8采用了尾插法,解决了这一问题,但是仍然会出现并发问题。

TreeMap线程不安全。

ConcurrentHashMap:底层采用分段锁机制(1.7)。synchronize+cas的自旋锁(1.8)机制解决并发安全问题。

7. JAVA8的ConcurrentHashMap为什么放弃了分段锁,有什么问题吗,如果你来设计,你如何 设计。

先说 JDK 1.7 是如何使用分段锁的。

Jdk1.7中, ConcurrentHashMap是由Segment+HashEntry实现存储。 通过二次hash找到需要 put 或者 get 的位置。 Segment是继承了ReentrantLock,一个Segment锁住若干个HashEntry.

**在实际生产环境中,map 在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待。**这是分段锁被弃用的最主要原因。

JDK1.8 的新方案:

使用Node锁来替代Segment,底层使用 synchronize来替代 ReentrantLocksync只需要锁住 Node[]节点即可。

好处:

  1. 减少内存开销
    假设使用ReentrantLock可重入来获得同步支持,那么每个节点都需要通过继承AQS来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。

  2. 获得JVM的支持
    可重入锁毕竟是API这个级别的,后续的性能优化空间很小。
    synchronized则是JVM直接支持的,JVM能够在运行时作出相应的优化措施:锁粗化、锁消除、锁自旋等等。这就使得synchronized能够随着JDK版本的升级而不改动代码的前提下获得性能上的提升。

8. 有没有 有顺序的Map类。如果有,他们是怎么保证有序的。

LinkedHashMap.

底层通过双向链表的方式实现插入的有序性。

LinkedHashMap构造函数,主要就是调用HashMap构造函数初始化了一个Entry[] table,然后调用自身的init初始化了一个只有头结点的双向链表。我们所有的put操作都是基于这个 头结点来完成。

9. 抽象类和接口的区别,类可以继承多个类么,接口可以继承多个接口么,类可以实现多个接口吗

抽象类(abstract):
顾名思义,抽象类就是一个类,普通类有的,它都有.与普通类唯一不同的是加了abstract修饰,且有可能含有抽象方法, so子类继承它的时候需实现abstract方法或者将子类也定义为抽象类

接口(interface):
完全不同于类,
属性全部是public static final
方法全部是public abstract
接口可以实现多继承
jdk1.8新特性:接口中方法可以有方法体,需修饰为default;接口可以有静态方法

10. 继承和聚合的区别在哪。

  • 继承指的是一个类继承另外的一个类的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系;在Java中此类关系通过关键字extends明确标识。

  • 聚合指的是聚合体现的是整体与部分、拥有的关系,此时整体与部分之间是可分离的,他们可以具有各自的生命周期;比如计算机与CPU、公司与员工的关系等;

11. IO 模型有哪些,讲讲你理解的 nio,他和 bio,aio 的区别是啥,谈谈 reactor 模型。

NIO BIO AIO 区别

BIO

同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程并处理,如果这个连接不做任何事情会造成不必要的开销,当然可以通过线程池机制改善。

NIO

同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理.

AIO

异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。

NIO基于Reactor,当socket有流可读或可写入socket,操作系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或写入操作系统。也就是,不是一个链接就要对应一个处理线程,而是一个有效请求对应一个线程,当连接没有数据时,是没有工作线程来处理的。

12. 反射的原理,反射创建类实例的三种方式是什么

反射的原理:

1、先明白 JVM 类的加载。

将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后再内存中创建一个java.lang.Class对象,用来封装类的方法区内的数据结构。

2、反射机制

Java的反射就是利用加载到jvm中的.class文件来进行操作的。.class文件中包含该类的所有信息,当你不知道某个类具体信息时,可以使用反射通过类的全限定名获取class,然后进行各种操作。

Java反射就是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;并且能改变它的属性。总结说:反射就是把java类中的各种成分映射成一个个的Java对象,并且可以进行操作。

获取类实例的三种方法:

package xx;
public class User{
    public User(){}
}

1、通过实例对象的getClass()方法

User user = new User();
Class clazz = user.getClass();

2、通过类的class属性

Class clazz = User.class;

3、通过类的全限定名,使用Class的静态方法forName()

Class clazz = Class.forName("xx.User");

13. 反射中,Class.forName 和 ClassLoader 区别。

(1)class.forName()除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块。当然还可以指定是否执行静态块。注意,forName()是不会执行静态方法的,但是会初始化静态变量。

(2)classLoader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

14. 描述动态代理的几种实现方式,分别说出相应的优缺点。

  1. 基于JDK实现的动态代理。

步骤:

  1. 实例化一个原始对象(被代理的对象)

  2. 调用Proxy.newIntance()方法生成代理类。

  3. 生成InvocationHandler接口实例对象

    重写其 invoke方法

    InvocationHandler handle = new InvocationHandler() {
         public Object invoke(Object proxy, Method method, Object[] args) throws 					Throwable {
                    System.out.println("-----------log---------");
                    Object invoke = method.invoke(userService, args);
                    return invoke;
                }
            };
    
  4. Proxy.newIntance(classLoader, interface, handle)传入三个参数

    classLoader:类加载器,需要一个类加载器来加载代理类(可以任意)

    interface: 原始对象实现的接口

    handle: InvocationHandler接口实例对象

  5. 代理类执行原始对象的方法,实现动态代理。

  1. 基于 CGLib 实现的动态代理

    CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,实现子类代理父类。

// 1. 创建原始对象
final UserService userService = new UserService();

// 2. 通过cglib创建代理对象
Enhancer enhancer = new Enhancer();
// 设置类加载器
enhancer.setClassLoader(TestCGLIB.class.getClassLoader());
// 设置原始对象为父类对象,生成一个代理子类
enhancer.setSuperclass(userService.getClass());

// 3. 创建MethodInterceptor实例,类似于上面的handle
MethodInterceptor methodInterceptor = new MethodInterceptor() {
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("cglib log----------");
        Object invoke = method.invoke(userService, objects);
        return invoke;
    }
};
// 4. 设置methodInterceptor
enhancer.setCallback(methodInterceptor);
// 5. 创建代理对象
UserService userService1 = (UserService) enhancer.create();
// 6. 执行代理
userService1.login("w","jb");
userService1.register(new User());

优缺点:

  • 使用JDK动态代理,目标类必须实现的某个接口,如果某个类没有实现接口则不能生成代理对象。

  • Cglib原理是针对目标类生成一个子类,覆盖其中的所有方法,所以目标类和方法不能声明为final类型。

  • Cglib可以强制实现对接口的动态代理

从执行效率上看,Cglib动态代理效率较高。

15. 动态代理的实现中 JDK和CGLib的区别

同上。

16. 为什么 CGlib 方式可以对接口实现代理。

不懂

17. final 的用途。

  • 修饰变量:表示一个常量
  • 修饰类:类不可被继承
  • 修饰方法:方法不可被重写

18. 写出三种单例模式实现。

  1. 经典饿汉式
// 构造器私有化
public class A{
    private static A instance = new A();
    private A(){}
    public static A getInstance(){
        return this.instance;
    }
}
  1. 经典懒汉式
// 1、双重检锁
// 2、volatile修饰防止指令重排
public class A{
    private static volatile A instance;
    private A(){}
    public static A getInstance(){
        if(instance == null){
            synchronized(A.class){
                if(instance == null)
                    instance = new A();
            }
        }
        return instance;
    }
}
  1. 枚举实现
public enum A{
    INSTANCE;
    public A getInstance(){
        return INSTANCE;
    }
}
  1. 静态内部类
public class A{
	private A(){}
    public static A getInstance(){
        return InnerClass.instance;
    }
    public static class InnerClass{
        private static final A instance = new A(); 
    }
}

20. 请结合 OO 设计理念,谈谈访问修饰符 public、private、protected、default 在应用设计中的作用。

访问修饰符,主要标示修饰块的作用域,方便隔离防护。

public: Java语言中访问限制最宽的修饰符,一般称之为“公共的”。被其修饰的类、属性以及方法不仅可以跨类访问,而且允许跨包(package)访问。

private: Java语言中对访问权限限制的最窄的修饰符,一般称之为“私有的”。被其修饰的类、属性以及方法只能被该类的对象访问,其子类不能访问,更不能允许跨包访问。

protect: 介于public 和 private 之间的一种访问修饰符,一般称之为“保护形”。被其修饰的类、属性以及方法只能被类本身的方法及子类访问,即使子类在不同的包中也可以访问。

default:即不加任何访问修饰符,通常称为“默认访问模式“。该模式下,只允许在同一个包中进行访问。

21. 深拷贝和浅拷贝区别

深拷贝和浅拷贝最根本的区别在于是否真正获取一个对象的复制实体,而不是引用

假设B复制了A,修改A的时候,看B是否发生变化:

如果B跟着也变了,说明是浅拷贝(修改堆内存中的同一个值, B是A的引用,指向同一块内存区域)

如果B没有改变,说明是深拷贝(修改堆内存中的不同的值,B是A的复制实体,指向不同的内存区域,B和A有不同的内存地址)

23. error 和 exception 的区别,CheckedException,RuntimeException 的区别。

借鉴:https://blog.csdn.net/riemann_/article/details/87522352

24. 请列出 5 个运行时异常。

ClassCastException(类转换异常)

IndexOutOfBoundsException(数组越界)

NullPointerException(空指针)

ArrayStoreException(数据存储异常,操作数组时类型不一致)

ConcurrentModifyException(并发修改异常)

25. 在自己的代码中,如果创建一个 java.lang.String 类,这个类是否可以被类加载器加载?为什么。

不可以。 因为 JVM类加载的双亲委派机制。 JVM在加载一个类的时候,会去委托该类的父类加载器去加载,父类再去委托父类,直到根类加载器。 如果根类加载器能够加载,那么它就会返回加载结果。

jdk原生的java.lang.String存在于根类加载器目录下,因此根类加载器可以加载该类,这就导致我们自己写的同名String类报错。(找不到Main函数)

26. 说一说你对 java.lang.Object 对象中 hashCode 和 equals 方法的理解。在什么场景下需要重新实现这两个方法。

这里有一个约定:hashCode相等,对象不一定相等,对象相等,hashCode一定相等。

为什么需要hashCode?
1、 在map等集合中,通过hashCode可以很快的找到元素的位置
2、比较两个对象是否相等时,先通过hashCode比较,这样速度比较快,如果不相等直接返回false

为什么要重载equal方法?
Object对象默认比较的是两个对象的内存地址是否一样,正常大家应该比较的是对象里面的值是否一样。

为什么重载hashCode方法?
如果我们只重写equals,而不重写hashCode方法,就会出现两个对象一样,但是hashCode不相等情况,在map等集合中应用时,就会出现问题,因为hashCode不一样,两个一样的对象会放到集合中。

27. 在 jdk1.5 中,引入了泛型,泛型的存在是用来解决什么问题

先明白转型问题。

Father f1 = new Son(); (向上转型, 父类引用指向子类对象) f1无法调用子类特有的方法

Son s1 = (Son)f1; (向下转型,指向子类对象的父类引用赋给子类)

Father f2 = new Father();

Son s2 = (Son)f2; //这样的向下转型编译无错,运行报错。

引入泛型就是为了解决这个问题。

泛型主要针对向下转型时所带来的安全隐患,其核心组成是在声明类或接口时,不设置参数或属性的类型。

30. Java 中的 HashSet 内部是如何工作的。

HashSet底层其实就是一个HashMapadd插入元素调用的就是HashMapput方法,其他的其实和HashMap大同小异。

31. 什么是序列化,怎么序列化,为什么序列化,反序列化会遇到什么问题,如何解决。

待补

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值