1.Java基础
1.1JAVA中的几种基本数据类型是什么,各自占用多少字节。
Java中有八种基本数据类型,分别是byte(1bit)、boolean(不占用)char(2bit)、short(2bit)、int(4bit)、long(8bit)、float(4bit)、double(8bit)。
1.2 String类能被继承吗,为什么。
String类不能被继承,因为其被final关键字声明,不允许被继承。
1.3 String,Stringbuffer,StringBuilder的区别。
- String是一个不可变的字符串,线程安全;
- StringBuffer和StringBuilder都是可变的字符串,与String主要的区别是允许修改其内容,StringBuffer其内部方法是同步的,线程安全;
- StringBuilder内部方法不是同步的,线程不安全;由于StringBuffer内部方法是通过synchronized关键字修饰,在获取锁和释放锁消耗性能,因此在操作效率上低于StringBuilder。
1.4 ArrayList和LinkedList有什么区别。
- ArrayList底层是由动态数组构建的,LinkedList底层是链表结构
- ArrayList由于其数组结构并实现了RandomAccess接口,可以实现随机访问,而LinkedList不能
- LinkedList在头插、尾插元素(某些情况)效率上比ArrayList效率高
- ArrayList相对于LinkedList占用更少的内存,由于LinkedList需要存储前驱和后驱指针
1.5 讲讲类的实例化顺序,比如父类静态数据,构造函数,父类字段,子类静态数据,构造函数,字段,当new的时候,他们的执行顺序。类静态数据----->子类静态数据----->父类字段----->父类构造方法----->子类字段----->子类构造方法
- 静态初始化块和字段只在类中初始化一次。
1.6 用过哪些Map类,都有什么区别,HashMap是线程安全的吗,并发下使用的Map是什么,他们内部原理分别是什么,比如存储方式,hashcode,扩容,默认容量等。
- 使用过HashMap、ConcurrentHashMap;HashMap是线程不安全的,ConcurrentHashMap是线程安全的。
- HashMap和ConcurrentHashMap底层结构都是数组+链表/红黑树组成的。
- HashMap默认容量为16,当集合中元素超过容量的0.75时,进行扩容,每次扩容增长为原来的两倍。为减少哈希冲突,需要将通过hashcode()方法计算得到的hash值与数组长度进行取余操作,将存储的元素散列到各个位置。如果除数为2的幂次,那么取余操作相当于除数减一的与操作,采用二进制与操作,能够提升运算效率。
- ConcurrentHashMap默认容量和扩容机制同HashMap,在JDK1.7时,其由Segment数组和HashEntry组成来保证多个线程同步,在JDK1.8以后,通过CAS操作和Node数组以及synchronized关键字保证每个Node数组之间的安全性(只要不产生hash冲突)。
1.7 JAVA8的ConcurrentHashMap为什么放弃了分段锁,有什么问题吗,如果你来设计,你如何设计。
JDK1.7的ConcurrentHashMap是利用Segment和HashEntry保证多个线程访问,Segment实现了Lock接口,是一种重量级锁,当数据分布不均匀时,会加剧锁竞争。使用CAS和局部重排序能够更细粒度实现线程同步,还能减少Segment分配的锁对象开销,提高性能。
1.8 有没有有顺序的Map实现类,如果有,他们是怎么保证有序的。
TreeMap就是有顺序的Map实现类,它是通过实现Comparator的compareTo方法保证有序
1.9 抽象类和接口的区别,类可以继承多个类么,接口可以继承多个接口么,类可以实现多个接口
区别:
- 接口强调对行为的约束,类强调的时从属关系;
- 一个类只能继承一个父类,但是可以实现多个接口
- 接口中的成员变量只能通过private static final修饰且必须初始化复制或构造函数赋值,抽象类中的成员变量是default类型,可由其子类重新定义或修改。
类不能继承多个类,接口可以继承多个接口,类可以实现多个接口
1.10 继承和聚合的区别在哪。
- 继承关注的是类之间的继承和扩展关系,而聚合关注的是类之间的组合和包含关系。
- 继承是一种强耦合关系,因为子类紧密依赖于父类的实现。而聚合是一种较弱的耦合关系,因为成员类可以独立于容器类存在和改变。
- 继承通常用于表示类之间的层次结构和抽象与具体的关系,而聚合用于表示类之间的组合和整体与部分的关系。
1.11 IO模型有哪些,讲讲你理解的nio ,他和bio,aio的区别是啥,谈谈reactor模型。
- IO模型有NIO(同步非阻塞)、BIO(同步阻塞)、AIO(异步非阻塞)三种IO模型
- BIO是指应用程序发起Read调用,会一直阻塞,直到内核把数据复制到应用层
- NIO是指同步非阻塞,它可以通过多次发起Read调用不阻塞,但当内核将数据拷贝到应用层是阻塞的。
- AIO基于事件和回调机制实现,应用操作后直接返回,当后台处理完,操作系统通知响应线程进行后续操作。
reactor模型
reactor模型是一种处理大量并发I/O请求的设计模式,常用于构建高性能服务器。
- 单个或多个输入处理器(Reactor):负责监听和分发来自客户端的事件或请求到相应的处理器。
- 事件处理器(Handler):负责处理实际的业务逻辑,如读取数据、执行计算、写回响应等。
- 多路复用:Reactor使用I/O多路复用技术(如select、poll、epoll等)来监听多个连接,而不是为每个连接分配一个线程。
- 事件驱动:系统基于事件驱动,当有新的事件发生时,Reactor会将事件分发给对应的处理器进行处理。
1.12 反射的原理,反射创建类实例的三种方式是什么。
反射是指运行时分析类,执行类中方法的机制。
反射创建实例的方法分为已知具体类和不知具体类
已知类使用类.class方法
不知类
- Class.forName(“全路径”)方法
- 对象实例instance.getClass
- 类加载ClassLoader.getSystemClassLoader.loaderClass(“全路径”)方法
1.13 反射中,Class.forName和ClassLoader区别 。
Class.forName()和ClassLoader都用于加载类,但它们之间还是存在区别:
- Class.forName()是一个静态方法,ClassLoader是一个类
- Class.forName()和ClassLoader类都可能在加载类时出现ClassNotFoundException,但ClassLoader拥有更多加载选项和更细粒度控制。
- Class.foName()加载的类如果没有初始化,那么它会通过Class.forName()进行初始化,ClassLoader默认不会初始化类,除非反射调用newIInstance()方法或其他初始化方法。
1.14 描述动态代理的几种实现方式,分别说出相应的优缺点。
JDK动态代理,代理接口,Proxy类的newProxyInstance()方法通过类加载器、接口、以及InvocationHandler对象以及重写InvocationHandler的invoke()方法实现动态代理
优点:
- 简单易用,只需重写InvocationHandler的invoke方法即可
- 无需引入额外库
- 目标类需要实现接口,才可创建代理对象
缺点:
- 目标类没有实现接口,无法使用JDK动态代理
- final方法和private方法,无法进行代理
CGLIB动态代理,代理类。通过字节码技术,子类覆盖父类实现代理
优点:
- 无需类实现接口就能实现动态代理
- 功能更强大,可代理final和private方法
缺点:
- 需引入额外库(CG)
- 相对于JDK代理更复杂,性能较差
- 通过生成子类方法实现,可能会产生继承的副作用。
1.15 动态代理与cglib实现的区别。
- JDK动态代理是通过实现InvocationHandler的invoke方法结合反射增强被代理类,再Proxy.newProxyInstance()方法根据接口和InvocationHandler对象产生代理对象完成代理功能;
- CGLIB动态代理是通过实现MethodInterceptor接口中的intercept()方法结合反射增强被代理类,通过Enhancer.create()方法产生代理对象完成代理功能。
1.16 为什么CGlib方式可以对接口实现代理。
其实CGlib是通过对接口的实现类实现动态代理,而不是对接口进行代理
1.17 final的用途。
final可以用来修饰成员变量、类,方法。被final修饰的变量一旦赋值不能被修改。被final修饰的类不能被继承,被final修饰的方法不能被重写。
1.18 写出三种单例模式实现 。
饿汉
public class Singleton{
private Singleton(){}
private static Singleton singleton = new Singleton();
private static Singleton getSingleton(){
return singleton;
}
}
懒汉式(线程不安全)
public class Singleton{
private Singleton(){}
private static Singleton singleton;
private static Singleton(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
双重检测懒汉式(线程安全)
public class Singleton{
private Singleton(){}
private static Singleton singleton;
private static Singleton(){
if(singleton == null){
sychronized(){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
枚举懒汉式
public enum Singleton{
INSTANCE;
}
1.19 如何在父类中为子类自动完成所有的hashcode和equals实现?这么做有何优劣。
在Java中,我们不能直接在父类中为子类自动完成所有hashCode和equals方法的实现,因为每个类可能具有不同的相等性和哈希码计算逻辑。但可以创建一个通用基类或抽象类,提供默认的、基于对象状态的hashCode和equals方法,并鼓励子类重写这些方法以适应自己的需求。
优点:
- 父类提供的通用方法做模板,减少重复代码
- 子类可以通过父类得到基本的equals方法和hashcode方法
缺点:
- 父类无法预知子类所有的字段,因此无法提供准确的equals方法和hashcode方法
- 子类有不相等性规则时,不正确的继承会导致违反equals和hashcode约定
1.20 请结合OO设计理念,谈谈访问修饰符public、private、protected、default在应用设计中的作用。
-
public:
public
访问修饰符表示成员(类、方法或变量)对所有其他类都是可见的,不受包或类层次结构的限制。
-
private:访问修饰符表示成员仅对其所在的类可见,对外部类完全不可见。常用于封装类的内部属性,只暴露必要的操作接口。
-
protected:
protected
访问修饰符允许成员在同一个包内或者子类中可见,但在包外的其他类中不可见。- 保护成员对于实现继承关系下的复用和扩展特别有用。子类能够访问父类的受保护成员,以便于进行重写(override)或者使用父类的非公共功能。
- 使用protected关键字有助于实现OO中的继承和多态特性,同时保持一定程度的封装。
-
default(无修饰符,也称为package-private):
- 默认访问修饰符没有显式指定,成员仅在同一个包内的其他类中可见,而在包外则不可见。
- 它支持同一包内的类之间共享某些功能,但不鼓励跨包间的直接访问,从而降低了不同包之间不必要的依赖关系。
- 在包级私有成员的帮助下,可以构建更模块化的系统,其中各个包相对独立,相互之间只通过公共API进行通信。
1.21 深拷贝和浅拷贝区别。
- 深拷贝和浅拷贝都是通过clone()方法拷贝父类引用。
- 深拷贝是拷贝原对象的整个对象,包括原对象的内部对象。
- 浅拷贝只是拷贝了原对象的内部对象的引用地址,浅拷贝对象和原对象还是共用同一个内部对象。
1.22 数组和链表数据结构描述,各自的时间复杂度。
- 数组是内存连续的,支持随机访问,插入删除效率较链表低
- 链表是内存不连续的,不支持随机访问,插入删除效率较数组高
1.23 error和exception的区别,CheckedException,RuntimeException的区别。
- error和Exception有一个共同的父类Throwable。
- error是应用程序无法处理的错误,不建议通过catch捕获,如虚拟机异常,Exception是程序可以处理的异常,可以使用catch进行捕获。
- 异常分为受检查异常(CheckedException)和不受检查异常(RuntimeException及其子类)。
- CheckedException是受检查异常,一定要捕获,不然无法编译通过
- RuntimeException及其子类被称为非受检查异常,不捕获也能编译通过
1.24 请列出5个运行时异常。
NullPointerException
(空指针错误)IllegalArgumentException
(参数错误比如方法入参类型错误)NumberFormatException
(字符串转换为数字格式错误,IllegalArgumentException
的子类)ArrayIndexOutOfBoundsException
(数组越界错误)ClassCastException
(类型转换错误)
1.25 在自己的代码中,如果创建一个java.lang.String类,这个类是否可以被类加载器加载?为什么。
不可以,String类是Java的核心类,由BootstrapClassLoader加载。Java类的加载遵循双亲委派加载机制。
1.26 说一说你对java.lang.Object对象中hashCode和equals方法的理解。在什么场景下需要重新实现这两个方法。
hashcode和equal方法都是用来判断两个对象是否相等,其中先进行hashcode判断,如果不等,则两个对象不相等,反之进一步通过equals方法比较两个对象内存地址是否相等(Object默认),实际应用中一一判断两个对象的属性值是否全部相等,进而判断两个对象是否相等。
场景:
- 类作为哈希表的键或值
- 基于字段判断对象是否相等
- 一旦重写equals方法,必须重写hashcode方法,保证两个相等的对象hash值相等。
1.27 在jdk1.5中,引入了泛型,泛型的存在是用来解决什么问题。
泛型在编译时,通过参数表示类型,再具体使用(运行)时实现类型转换。
1.28 这样的a.hashcode() 有什么用,与a.equals(b)有什么关系。
求对象的hash值,在使用equals比较两个对象是否相等时,会先计算两个对象的hashcode是否相等,如果不等则两个对象不相等,如果相同,则进一步通过equals方法比较两个对象的内存地址判断两个对象是否相等(Object默认),实际应用是一一判断两个对象的属性值是否全部相等。
1.29 有没有可能2个不相等的对象有相同的hashcode。
有可能,因为两个对象并不是具有相同的hashcode就是同一对象,这还需要通过equals()方法比较两个对象中内存地址相等(Object默认),实际应用中一一判断两个对象的全部属性值是否相等,才能决定两个对象是否相等。
1.30 Java中的HashSet内部是如何工作的。
HashSet用于存储唯一且不重复的元素,当一个元素加入HashSet时,会先根据哈希算法计算该元素的hash值,如果hash值与HashSet中已加入元素的hash值不同,则加入到HashSet中;反之则进一步通过元素的equals方法计算它们是否相等,如果相等则该元素已存在,不必加入HashSet中;反之则通过拉链法解决哈希冲突,将元素以链表或红黑树形式链接。
1.31 什么是序列化,怎么序列化,为什么序列化,反序列化会遇到什么问题,如何解决。
序列化是将对象转换为二进制字节流进行数据传输。在Java中类可以通过实现Serializable接口实现序列化,也可以引入其他序列化协议进行序列化,如kryo、protobuf、Hession等。网络数据传输、文件存储、数据库、内存存储都是以二进制字节流流进行存储。如果我们需要将对象进行网络数据传输、文件、数据库、内存存储就需要进行序列化。
反序列化问题:
安全问题:反序列化前需要对数据进行验证和过滤
兼容性问题:当对类中字段进行增删改时,序列化的类版本与运行时类版本不一致导致序列化异常或数据丢失;对于实现了Serializable但是没有自定义serialVersionUID
的类,Java会自动生成一个序列化号,在改变类结构时容易出现序列化失败。
性能问题:大量数据反序列化会消耗大量内存和CPU资源,可通过分块读取和处理数据、合理设置缓冲区大小。
异常处理:在反序列化时会出现ClassNotFoundException、InvalidClassException(类格式错误或版本不匹配)、IOException,应妥善处理这些异常,保证序列化健壮性。
数据一致性问题:序列化包含无法持久化的引用(网络连接),反序列化会导致无效状态。
1.32 java8的新特性
- lambda表达式:简洁方式定义匿名函数,提高代码可读性,简化集合流操作、事件处理
- Stream流:简化集合的排序、过滤、映射、聚合操作
- removeIf:简化集合中元素删除操作