文章目录
- 1、基础概念
- 2、Java 泛型
- 3、面向对象
- # 面向对象和面向过程的区别
- # 定义一个不做事且无参构造方法的作用?
- # Java中的重写和重载的区别?
- # 成员变量和局部变量的区别?
- # 在一个静态方法内调用一个非静态成员为什么是非法的?
- # 静态方法和实例方法有什么不同?
- # Java的static静态方法和实例方法(非静态方法)用synchronized修饰有什么区别?
- # 同一个对象在两个线程中调用synchronized修饰的static方法和用synchronized修饰的实例方法会阻塞吗?
- # Java程序初始化顺序是怎样的?
- # 对象拷贝
- # Java 的 fail-fast 和 fail-safe 有啥区别?
- # Java中有哪些引用?弱引用和软引用区别?
- 2.软引用
- 3.弱引用
- 4.虚引用
- # Java中对象类型转换
- # 如何实现数组和 List 之间的转换?
- # 为什么Java中只有值传递?
- # 为什么设计包装类?
- # 自动拆箱和自动装箱
- # `Integer.valueOf(100) == Integer.valueOf(100)`,true还是false,为什么?
- # 缓存池
- 4、String类
- 5、异常
- 6、多线程
- 7、文件与I/O流
1、基础概念
# Java 语言有什么特点?
简单易学、面向对象(封装、继承、多态)、编译与解释并存(编译器和解释器)、可靠性、安全性、平台无关性(JVM 实现平台无关性)、多线程、支持网络编程并且更加方便(简化网络编程)。
-
为什么说Java是编译与解释并存?
高级编程语言按照程序的执行方式分为编译型和解释性
- 编译型:编译器针对特定的操作系统将源码一次性翻译成可被该平台执行的机器码;
- 解释型:解释器对源程序逐行解释成特定平台的机器码并立即执行。
Java 程序先经过编译(生成字节码),后解释(前面生成的字节码需要Java 解释器解释执行)两个步骤;
# Java 和 C++的区别?
- 都是面向对象的语言,都支持封装、继承和多态
- Java 不提供指针来直接访问内存,程序内存更加安全
- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
- Java 有自动内存管理机制,不需要程序员手动释放无用内存
- 在 C 语言中,字符串或字符数组最后都会有一个额外的字符‘\0’来表示结束。(Java中类实现trim()方法,该方法能将字符串前后的空字符去掉)
# Java 面向对象编程三大特性?
封装、继承、多态
1.封装
封装就是把抽象的数据和对数据进行的操作封装在一起,数据被保存在内部,程序的其他部分只有通过被授权的操作(成员方法)才能对数据进行操作。
java提供了四种控制修饰符控制方法和变量的访问权限:
- public:对外公开
- protected:对子类和同一包中的类公开
- 没有修饰符号:向同一个包的类公开
- private:只有类本身可以访问,不对外公开
2.继承(extends )
继承是使用已存在的类的定义作为基础建立新类的技术。继承可以解决代码复用问题,当多个类存在相同的属性(变量)和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过extend语句来声明继承父类。
关于继承如下 3 点请记住:
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
3.多态:(重载、重写)
所谓多态,就是指一个引用(类型)在不同情况下的多种状态,你也可以这样理解:父类型的引用指向子类型的对象。
多态有两个好处:
- 应用程序不必为每一个派生类编写功能调用,只需要对抽象基类进行处理即可。大大提高程序的可复用性。//继承
- 派生类的功能可以被基类的方法或引用变量所调用,这叫向后兼容,可以提高可扩充性和可维护性。 //多态的真正作用
多态的特点:
- 对象类型和引用类型之间具有继承或者实现的关系
- 对象类型不可变,引用类型可变
- 方法具有多态性,属性不具有
- 引用类型变量发出的方法调用的到底是哪一个类中的方法,必须在程序运行期间才能确定
- 多态不能调用“只在子类存在但在父类不存在的方法”
- 如果子类重写类父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法
# 什么是字节码?采用字节码的好处是什么?
在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class
的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。
- Java 程序从源代码到运行一般有下面三步:
# JDK 和 JRE 有什么区别?
Java程序是运行在JVM(Java虚拟机)上的,在开发程序之前要配置Java开发环境,而配置环境要做的就是JDK的安装和配置。简单来说就是JDK包含JRE,JRE又包含JVM的关系,也可以说JDK是JRE + Java 开发工具;JRE包含了JVM + Java 语言的核心类库。
1. JDK的简单认识
- Java Development Kit 是Java的标准开发工具包;
- 它提供了编译、运行Java程序所需的各种工具和资源,包括Java编译器、Java运行环境JRE,以及常用的Java基础类库等,是整个JAVA的核心。
2. JRE的简单认识
- Java runtime environment 是运行基于Java语言编写的程序所不可缺少的运行环境,用于解释执行Java的字节码文件。
- JRE中包含了Java virtual machine(JVM)、runtime class libraries和 Java application launcher,这些是运行 Java 程序的必要组件。
- 与JDK不同,JRE是Java运行环境,并不是一个开发环境,所以没有包含任何开发工具(如编译器和调试器),只是针对使用Java程序的用户。
3. JVM(Java Virtual Machine)的简单认识
- JVM 是运行Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Win、Linux、macOS),目的是使用相同的字节码,都给出相同的结果
- 它是整个java实现跨平台的最核心的部分,负责解释执行字节码文件,是可运行java字节码文件的虚拟计算机。正是因为有了JVM的存在,Java才实现了强大的跨平台特性。
# OracleJDK 和 OpenJDK 的对⽐
- OpenJDK 是一个参考模型并且是完全开源的,而OracleJDK是OpenJDK的一个实现,并不是完全开源的。
- OracleJDK 更稳定,在响应性和JVM性能方面有更好的性能
2、Java 泛型
# Java 泛型的实现方法:类型擦除
Java的泛型是伪泛型,因为Java在编译期间,所有的泛型信息都会被擦掉。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。
- 证明类型擦除
public class Test {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<String>();
list1.add("abc");
ArrayList<Integer> list2 = new ArrayList<Integer>();
list2.add(123);
System.out.println(list1.getClass() == list2.getClass());
//返回结果是true,说明泛型类型String和Integer都被擦除掉了,只剩下原始类型。
//原始类型就是擦除泛型信息后,最后在字节码中的类型变量的真正类型;
}
}
# 类型擦除引起的问题及解决方法
TODO
3、面向对象
# 面向对象和面向过程的区别
- 面向过程:面向过程性能比面向对象高,因为面向对象需要实例化对象,开销比较大;面向过程没有面向对象易维护、易复用、易扩展
- 面向对象:向向对象易维护、易复用、易扩展,因为面向对象三大特性:封装、继承、多态
# 定义一个不做事且无参构造方法的作用?
Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会默认调用父类中没有参数的构造方法。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。
- 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是帮助子类做初始化工作
# Java中的重写和重载的区别?
1.重写
- 定义:子类对父类中允许访问的方法的**方法体(即方法逻辑)**进行重新编写。注意重写发生在运行期。
- 意义:增强了类的复用性(即子类可以通过继承拿到父类的东西)、扩展性(即子类按照自己的需求加入自己的东西)
- 规则:
- 方法名、参数列表、返回值类型必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
- 如果父类方法访问修饰符为
private/final/static
则子类就不能重写该方法,但是被static
修饰的方法能够被再次声明。 - 构造方法无法被重写
2.重载
- 定义:定义在同一类中的两个或多个方法同名但不同参,这就叫做方法的重载。
- 规则
- 方法名必须相同、参数列表必须不同(参数的个数或类型不同)、返回值类型可以相同也可以不同。
# 成员变量和局部变量的区别?
- 从语法形式上看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被
public
,private
,static
等修饰符所修饰,而局部变量不能被访问控制修饰符及static
所修饰;但是,成员变量和局部变量都能被final
所修饰。 - 从变量在内存中的存储方式看:如果成员变量是使用
static
修饰的,那么这个成员变量是属于类的,如果没有使用static
修饰,这个成员变量是属于实例的。对象存于堆内存,如果局部变量类型为基本数据类型,那么存储在栈内存,如果为引用数据类型,那存放的是指向堆内存对象的引用或者是指向常量池中的地址。 - 从变量在内存中的生存时间看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
- 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(例外:被
final
修饰的成员变量必须显式地赋值),而局部变量则不会自动赋值,必须被显式的赋值。
1.成员变量
- 实例变量:不用static修饰
- 类变量:用static修饰
2.局部变量
包括形参,方法局部变量,代码块局部变量,存在于方法的参数列表和方法定义中以及代码块中。
# 在一个静态方法内调用一个非静态成员为什么是非法的?
- 静态成员变量和方法属于类本身,在类加载的时候就会分配内存,可以通过类名直接访问。
- 非静态成员变量或方法属于类的对象,只有在类的对象产生时(实例化时)才会分配内存,然后通过类的对象(实例)访问。
- 因此在一个静态方法内调用一个非静态成员时,可能会出现非静态成员没分配内存(内存中不存在),非静态成员还不存在的情况,此时就会出错,因此是非法的。
# 静态方法和实例方法有什么不同?
- 访问方式:
- 在外部调用静态方法时,可以使用类名.方法名的方式,也可以使用对象名.方法名的方式。
- 而实例方法只有使用对象名.方法名的方式。也就是说,调用静态方法可以无需创建对象。
- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制
# Java的static静态方法和实例方法(非静态方法)用synchronized修饰有什么区别?
1.Synchronized修饰静态(static)方法,实际上是对该类对象加锁,俗称“类锁”。
- 用类直接在两个线程中调用两个不同的同步方法,互斥
因为对静态对象加锁实际上对类(.class)加锁,类对象只有一个,可以理解为任何时候都只有一个空间,里面有N个房间,一把锁,因此房间(同步方法)之间一定是互斥的。
2.Synchronized修饰非静态方法,实际上是对调用该方法的对象加锁,俗称“对象锁”。
- 同一个对象在两个线程中分别访问该对象的两个同步方法,互斥
因为锁针对的是对象,当对象调用一个synchronized方法时,其他同步方法需要等待其执行结束并释放锁后才能执行。 - 不同对象在两个线程中调用同一个同步方法,不会互斥
因为是两个对象,就相当于两个大房子,彼此之间互不干扰,具有两把钥匙。锁针对的是对象,并不是方法,所以可以并发执行,不会互斥。形象的来说就是因为我们每个线程在调用方法的时候都是new 一个对象,那么就会出现两个空间,两把钥匙。
# 同一个对象在两个线程中调用synchronized修饰的static方法和用synchronized修饰的实例方法会阻塞吗?
不会,因为锁的对象是不一样的,一个锁的类对象,一个是实例对象。
# Java程序初始化顺序是怎样的?
1.Java 程序的初始化一般遵循 3 个原则(优先级依次递减):
- ①静态对象(变量)优先于非静态对象(变量)初始化,其中,静态对象(变量)只初始化一次,而非静态对象(变量)可能会初始化多次。
- ②父类优先于子类进行初始化。
- ③按照成员变量的定义顺序进行初始化。即使变量定义散布于方法定义之中,它们依然在任何方法(包括构造函数)被调用之前先初始化。
2.Java程序初始化执行的顺序如下:
- 父类静态变量、父类静态代码块、
- 子类静态变量、子类静态代码块、
- 父类非静态变量、父类非静态代码块、父类构造函数、
- 子类非静态变量、子类非静态代码块、子类构造函数。
# 对象拷贝
对象拷贝是一种创建对象精确副本的方法。对象类的 clone
方法就是用于拷贝对象的。
1.为什么需要拷贝对象?直接new一个对象不可以吗?
答:拷贝的对象可能包含一些已经修改过的属性,而 new 出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的“状态”就靠 clone
方法了。那我们也可以先new一个对象,然后把需要保存的对象的属性一个一个复制给新的对象呀?首先这样做肯定是可以的,但问题在在于,一是操作繁琐,而是我们调用 clone
方法进行对象克隆,是 native
方法,底层实现,速度快。还需要注意的是,通过 clone
方法赋值的对象与原对象同时独立存在。
2.对象拷贝的分类
- 浅拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其副本引用同一个对象。
- 深拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,无论该字段是值类型的还是引用类型,都复制独立的一份。当你修改其中一个对象的任何内容时,都不会影响另一个对象的内容。
3.如何实现对象拷贝?
- 通过实现
Cloneabe
接口并重写Object
类中的clone()
方法可以实现浅克隆。 - 实现
Serializable
接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。深拷贝常见有以下四种实现方式:- 构造函数
Serializable
序列化- 实现
Cloneable
接口 JSON
序列化
4.目前常用的对象拷贝工具
目前常用属性拷贝工具,包括 Apache
的 BeanUtils
、Spring
的 BeanUtils
、Cglib
的 BeanCopier
、mapstruct
。
Apache BeanUtils
:用于简化对javaBean
的操作,能够对基本类型自动转换。Spring BeanUtils
:spring
项目可以直接使用。Cglib BeanCopier
:cglib(Code Generation Library)
是一个强大的、高性能、高质量的代码生成类库,BeanCopier
依托于cglib
的字节码增强能力,动态生成实现类,完成对象的拷贝。mapstruct
:mapstruct
是一个Java
注释处理器,用于生成类型安全的bean
映射类,在构建时,根据注解生成实现类,完成对象拷贝。
# Java 的 fail-fast 和 fail-safe 有啥区别?
fail-safe 允许在遍历的过程中对容器中的数据(复制一份)进行修改,而 fail-fast 则不允许。
- fail-fast 快速失败:使用迭代器对集合对象进行遍历时,如果A线程正在进行遍历,正好B线程对集合进行了修改,那么A线程则会抛出
ConcurrentModificationException
,迭代器在遍历时直接访问集合中的内容,并且在遍历的时候会使用一个modCount
的变量。集合在被遍历期间,如果内容发生变换,或导致modCount
这个变量发生变换。因此迭代器在每一次使用hashNext()
或者next()
遍历下一个元素之前,都会检查这modCount
的值是否是expectedmodCount
值,是则继续遍历,否则抛出异常。快速失败是不安全的,因为B线程在修改的过程中,能够把modCount
值修改成expectedModCount
,使得异常无法正确的抛出。 - fail-safe 安全失败:采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有的集合内容,在拷贝的集合上进行遍历。安全失败的原理:由于迭代是在拷贝的集合上进行遍历的,因此原集合内容的修改不会被迭代器检测到,所以不会触发异常。
# Java中有哪些引用?弱引用和软引用区别?
1.强引用
- 在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。
- 当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到, JVM 也不会回收。
- 当内存空间不足时,
Java
虚拟机宁愿抛出OutOfMemoryError
错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。 - 因此强引用是造成 Java 内存泄漏的主要原因之一。
- 如果强引用对象不适用时,需要弱化从而使GC能够回收,比如将
Object strongReference = new Object();
中的strongReference
显式地设置为 null,或让其超出对象的生命周期范围,则GC
认为该对象不存在引用,这时就可以回收这个对象。具体什么时候收集这要取决于GC
算法。
2.软引用
- 如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可以用来实现内存敏感的高速缓存。
- 软引用可以和一个引用队列(
ReferenceQueue
)联合使用。如果软引用所引用对象被垃圾回收,JAVA
虚拟机就会把这个软引用加入到与之关联的引用队列中。也就是说,垃圾收集线程会在虚拟机抛出OutOfMemoryError
之前回收软引用对象,而且虚拟机会尽可能优先回收长时间闲置不用的软引用对象。对那些刚构建的或刚使用过的“较新的”*软对象会被虚拟机尽可能保留,这就是引入引用队列ReferenceQueue
的原因。
3.弱引用
- 弱引用比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。【ThreadLocal】
- 弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。
4.虚引用
- 虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
- 它不能单独使用,必须和引用队列联合使用。
- 应用场景:虚引用主要用来跟踪对象被垃圾回收器回收的活动。
# Java中对象类型转换
1.类型转换
将一个类型强制转换成另一个类型的过程
2.对象类型转换
对象类型转换针对的是存在继承关系的对象,并不是任意类型的对象;Java语言允许某个类型的引用变量引用子类的实例,而且可以对这个引用变量进行类型转换。Java中对象类型转换主要分为向上转型和向下转型。
- 向上转型:父类引用指向子类对象,即通过子类对象(小范围)实例化父类对象(大范围),这种属于自动转换。使用向上转型可以调用父类类型中的所有成员,不能调用子类类型中特有成员。
- 向下转型:子类引用指向父类对象,即通过父类对象(大范围)实例化子类对象(小范围),这种属于强制转换。使用向下转型可以调用子类类型中所有的成员。
# 如何实现数组和 List 之间的转换?
- 数组转 List:使用
Arrays.asList(array)
进行转换。 - List 转数组:使用 List 自带的
toArray()
方法。
// array to list
String[] array = new String[]{"王磊","的博客"};
Arrays.asList(array);
// list to array
List<String> list = new ArrayList<String>();
list.add("王磊");
list.add("的博客");
list.toArray();
# 为什么Java中只有值传递?
- Java 的参数是以值传递的形式传入方法中,而不是引用传递。
- 在传递引用变量时,本质上是将对象的地址以值的方式传递到形参中。
# 为什么设计包装类?
Java 中,万物皆对象,所有的操作都要求用对象的形式进行描述。但是 Java 中除了对象(引用类型)还有八大基本数据类型,它们不是对象。那么,为了把基本数据类型转换成对象,最简单的做法就是将基本数据类型作为一个类的属性保存起来,也就是把基本数据类型包装一下,这也就是包装类的由来。
- 包装类还为基本类型添加了属性和方法,丰富了基本类型的操作。
- 基本数据类型不能向上转型获取到Object提供的方法,导致无法参与转型、泛型、反射等过程。
1.包装类的作用
- 集合中不允许存放基本数据类型,所以常用包装类。
- 包装类包含每种基本类型的相关属性,如最大值,最小值,所占位数以及相关的操作方法等。
- 包装类作为基本数据类型对应的类型,提供了一系列实用的对象操作,如类型转换和进制转换。
2.那又为什么还要让基本数据类型存在呢?
- Java语言中,用new关键字创建的对象是存储在堆里的,通过栈中的引用来使用这些对象。所以,对象本身是比较消耗资源的。
- 对于常用到的类型,如
int
等,如果我们每次使用这种变量的时候都需要new
一个对象的话,会比较笨重。所以,Java提供了基本数据类型,不需要使用new
在堆上创建,而是直接在栈内存中存储,因此会更加高效。
# 自动拆箱和自动装箱
- 装箱:将基本数据类型用它对应的包装类型包装起来
- 拆箱:将包装类型转换为对应的基本数据类型
Java 使用自动装箱和拆箱机制,节省了常用数值的内存开销和创建对象的开销,提高了效率,由编译器来完成,编译器会在编译期根据语法决定是否进行装箱和拆箱动作。
1.int和Integer的区别?
- int 是基本数据类型,Integer 是 int 的包装类,就是将 int 类型包装成 Object 对象;
- Integer 变量必须实例化后才能使用;int 变量不需要;
- Integer 实际是对象的引用,指向此 new 的 Integer 对象;int 直接存储数据值在常量池;
- Integer 的默认值是 null ;int 的默认值是0。
深入理解:
- 两个通过 new 生成的 Integer 变量永远是不相等的。因为 new 生成的是两个对象,其内存地址不同。Integer 与 new Integer不会相等。因为非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同。
- 两个都是非new出来的Integer,如果数在-128到127之间(会使用缓存池),则是true,否则为false。
- java 在编译
Integer i = 127
的时候,被翻译成Integer i = Integer.valueOf(127)
;java API中对Integer
类型的valueOf
的定义如下,对于-128到127之间的数,会进行缓存,Integer i = 127
时,会将127这个Integer
对象进行缓存,下次再写Integer j = 127
时,就会直接从缓存中取,就不会new
了。 Integer
变量和int
变量比较时,只要两个变量的值是相等的,则结果为true
。(因为包装类Integer
和基本数据类型int
比较时,java会自动拆箱为int,然后进行比较,实际上就变为两个int变量的比较)Integer.valueOf
函数源码:
public static Integer valueOf(int i) {
return i >= 128 || i < -128 ? new Integer(i) : SMALL_VALUES[i + 128];
}
2.为什么有了 int
还要设计 Integer
呢?
- 对象封装好处,可以把属性(也就是数据)跟处理这些数据的方法结合在一起,比如
Integer
就有parseInt()
等方法来专门处理int
型相关的数据。 - 另一个非常重要的原因就是在Java中绝大部分方法或类都是用来处理类类型对象的,如
ArrayList
集合类就只能以类作为他的存储对象,而这时如果想把一个int
型的数据存入list
是不可能的,必须把它包装成类Integer
才能被List
所接受。所以Integer
的存在是很必要的。
# Integer.valueOf(100) == Integer.valueOf(100)
,true还是false,为什么?
new Integer(100)
每次都会新建一个对象;Integer.valueOf(100)
会使用缓存池(-128,127)中的对象,多次调用会取得同一个对象的缓存。并且 Integer a = 100
;此时会编译时自动变成,Integer a = Integer.valueOf(100)
;
口诀:
- Integer变量 和 new Integer() 变量比较 ,永远为 false。
- 两个Integer 变量比较,如果两个变量的值在区间-128到127 之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为 false 。
- int 变量 与 Integer、 new Integer() 比较时,对象会自动拆箱,所以只要两个的值是相等,则为true
Integer a = Integer.valueOf(100);
Integer b = 100;
Integer c = new Integer(100);
Integer d = 128;
Integer e = Integer.valueOf(128);
Integer f = Integer.valueOf(128);
Integer g = new Integer(128);
Integer i = new Integer(127);
int j = 127; //相等,这里是自动装箱机制
Integer x = new Integer(128);
int y = 128;
System.out.println(a==b); //true
System.out.println(a==c); //false
System.out.println(b==c); //false
System.out.println(d==e); //false
System.out.println(e==f); //false
System.out.println(g==f); //false
System.out.println(d.equals(e)); //true
System.out.println("=========");
System.out.println(i==j); //true
System.out.println(x==y); //true
# 缓存池
new Integer(123)
与 Integer.valueOf(123)
的区别在于:
new Integer(123)
每次都会新建一个对象;Integer.valueOf(123)
会使用缓存池中的对象,多次调用会取得同一个对象的引用。
valueOf()
方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。
在 Java 8 中,Integer
缓存池的大小默认为 -128~127。
基本类型对应的缓冲池如下:
- boolean values true and false
- all byte values
- short values between -128 and 127
- int values between -128 and 127 (Java8中Integer的缓冲池上界可调。)
- char in the range \u0000 to \u007F
总结:在使用这些基本类型对应的包装类型时,如果该数值范围在缓冲池范围内,就可以直接使用缓冲池中的对象。
4、String类
String
被声明为final
,因此它不可被继承。在 Java8 中,String 内部使用 char 数组存储数据。String
类内部定义了final char value[]
用于存储字符串数据,value 是一个 final 类型,不可以修改,即 value不 能指向新的地址,但是单个字符内容是可以变化的。String
实现了Serializable
接口:表示字符串是支持序列化的,说明可以在网络上传输。实现了Comparable
接口:表示 String 可以比较大小。
1.String 类不可变的好处
- 可以缓存
hash
值,不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算 - 字符常量池(
String Pool
)的需要,通过字面量的方式给一个String
对象赋值,此时的字符串值声明再字符常量池中,字符常量池只能够不会存储相同内容的字符串。只有不可变的对象才能进行缓存,如包装类、String。 - 安全性,
String
经常作为参数,String
不可变性可以保证参数的不可变性 - 线程安全,
String
类的不可变性天生具备线程安全,可以在多线程中安全地使用。
2.String、StringBuffer and StringBuilder三者的比较
String 类的不可变性,导致了每次对String对象的操作都会产生新的String对象,浪费内存空间并效率低下,所以就有了 StringBuffer 和 StringBuilder ,它们对应的对象可以多次被修改,且不会产生新的对象。
- String 类是不可变的,StringBuffer 和 StringBuilder 都是可变的;
- String 类和 StringBuffer 类是线程安全的,StringBuilder 类是线程不安全的,但StringBuilder 相较于 StringBuffer 有速度的优势,在要求线程安全的情况下必须使用StringBuffer,其余情况等于考虑效率那就使用StringBuilder。
对三者的总结:
- 操作少量的数据:String
- 单线程操作字符串缓冲区下操作大量数据:StringBuilder
- 多线程操作字符串缓存区下操作大量数据:StringBuffer
# == 和 equals 的区别是什么?
- ==:作用是判断两个对象是不是同一个对象
- 对于基本类型和引用类型 == 的作用效果是不同的,其本质比较的都是值,如下所示:
- 基本类型:比较的是值是否相同;
- 引用类型:比较的是引用是否相同,也就是内存地址值是否相同;
- 如果类没有覆盖
equals()
那它本质上就是 ==,只不过String
和Integer
等重写了equals()
方法,把它变成了对象的值比较。- 默认情况下,
equals()
比较两个有相同值的对象,结果是false
的。 - 两个有相同值的
String
对象,二者的引用是不同的,但它们的equals()
结果是true
,因为String
重写了Object
的equals()
方法,将它从引用比较变成了值比较。
等价与相等
- 默认情况下,
- 对于基本类型,== 判断两个值是否相等,基本类型没有
equals()
方法。 - 对于引用类型,== 判断两个变量是否引用同一个对象,而
equals()
判断引用的对象是否等价。
# equals() 的作用是什么?
用来判断两个对象是否相等。通过判断两个对象的地址是否相等(即是否是同一个对象)来区分它们是否相等。
public boolean equals(Object obj) {
return (this == obj);
}
Object.java中定义了equals()
方法,意味着所有的Java类都实现了equals()
方法,所有的类都可以通过equals()
去比较两个对象是否相等。 但是,使用默认的“equals()”方法,等价于“==”方法。因此,通常会重写 equals()
方法:若两个对象的内容相等,则 equals()
方法返回true;否则,返回fasle。
- 根据“类是否覆盖equals()方法”,将它分为2类
- 没有覆盖
equals()
方法,实际上是比较两个对象是不是同一个对象。等价于通过“==”去比较这两个对象。 - 覆盖类的
equals()
方法,让equals()
通过其它方式比较两个对象是否相等。通常的做法是:若两个对象的内容相等,则 mequals()
方法返回true;否则,返回fasle。
- 没有覆盖
# hashCode() 的作用是什么?
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。
# 重写hashCode就要重写equals吗,反过来呢,为什么?
重写hashCode不一定要重写equals,重写equals就一定要重写hashCode!
- 前者如果不那么做,就违背了
hashCode
的通用约定,即如果equals
比较两个对象相等了,那么这两个对象就必须有相同的hash
值。
众所周知,根据生成的哈希码将数据离散开来,可以使存取元素更快。调用 Object.hashCode() 生成哈希值;由于不可避免地会存在哈希值冲突的情况,因此当 hashCode 相同时,还需要再调用 equals 进行一次值的比较;但是,若hashCode
不同,将直接判定 Object 不同,跳过equals
,这加快了冲突处理效率。 - 注意:
- hashCode-保证在最快的时间内判断两个对象是否相等,可能有误差值。【保证性能】
- equals-保证比较对象是否是绝对相等的。【保证可靠】(当hashCode值相同时,再进行equals比较,否则直接跳过)
- 如果hashCode和equals都不重写,则存放对象时调用的是Object对象的这两个方法。那么hashcode就是地址值,而equals也是比较的地址值。
# 反射
Reflection(反射)是被视为动态语言的关键
Java的反射机制是指在程序的运行状态中,借助于 ReflectionAPI,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。
加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为:反射。
- Java反射机制提供的功能
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时判断任意一个类所具有的成员变量和方法
- 在运行时获取泛型信息,在运行时调用任意一个对象的成员变量和方法
- 在运行时处理注解,生成动态代理
- 反射技术大量用于Java设计模式和框架技术,最常见的设计模式就是工厂模式和单例模式。
- 单例模式(Singleton):这个模式主要作用是保证在Java应用程序中,一个类Class只有一个实例存在。在很多操作中,比如建立目录 数据库连接都需要这样的单线程操作。这样做就是为了节省内存空间,保证我们所访问到的都是同一个对象。
- 工厂模式(Factory):工厂模式利用Java反射机制和Java多态的特性可以让程序更加具有灵活性。用工厂模式进行大型项目的开发,可以很好的进行项目并行开发。
# 什么是 Java 序列化?什么情况下需要序列化?
Java 序列化是为了保存各种对象在内存中的状态到本地磁盘,并且可以把保存的对象状态再读出来。一般在以下情况进行序列化:持久化,复制,传输
- 当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;(持久化)
- 当你想用套接字在网络上传送对象的时候;(传输)
- 当你想通过RMI传输对象的时候;
1.Java序列化中如果有些字段不想进行序列化吗,怎么办?
使用transient
关键字修饰,其阻止实例中那些用此关键字修饰的变量序列化;当对象被反序列化时,被transient
修饰的变量值不会被持久化和恢复。transient
只能修饰变量,不能修饰类和方法。
2.如何将一个 Java 对象序列化到文件里
在 java 中能够被序列化的类必须先实现 Serializable 接口,该接口没有任何抽象方法只是起到一个标记作用
- 对象需要实现Serializable接口
- 通过ObjectOutputStream的writeObject()方法写入和ObjectInputStream的readObject()方法来进行读取
5、异常
- Error是程序不能处理的错误、
- Exception是程序本身可以处理的异常
1.throw 和 throws 的区别?
- throw:是真实抛出一个异常。
- throws:是声明可能会抛出一个异常。
不同点:
- 位置不同。
throws
用在函数上,后边跟的是异常类,可以跟多个异常类。throw
用在函数内,后面跟的是异常对象。 - 功能不同。
- ①
throws
用来声明异常,让调用者只知道该功能可能出现的问题,可以给出预先处理方式。throw
抛出具体的问题对象,执行到throw
。功能就已经结束了跳转到调用者,并将具体的问题对象抛给调用者,也就是说throw
语句独立存在时,下面不要定义其他语句,因为执行不到。 - ②
throws
表示出现异常的一种可能性,并不一定会发生这些异常,throw
则是抛出了异常,执行throw
则一定抛出了某种异常对象。
相同点:两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常。
- ①
2.final、finally、finalize 有什么区别?
- final:是修饰符。修饰类,此类不能被继承;修饰方法和变量,则表示此方法和此变量不能在被改变,只能使用。
- finally:是 try{} catch{} finally{} 最后一部分,是在异常处理时提供finally块来执行任何清除操作,不管有没有异常被抛出、捕获,finally块都会被执行。finall块是可以省略的,但如果 finally 部分存在,则一定会执行 finally 里面的代码。
- finalize: 是 Object 类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法。
3.try-catch-finally 中哪个部分可以省略?
try-catch-finally 其中 catch 和 finally 都可以被省略,但是不能同时省略,也就是说有 try 的时候,必须后面跟一个 catch 或者 finally。
4.try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?
finally 一定会执行,即使是 catch 中 return 了,catch 中的 return 会等 finally 中的代码执行完之后,才会执行。而且如果finally代码块中含有return语句,会覆盖其他地方的return。
5.常见的异常类有哪些?
- NullPointerException 空指针异常
- ClassNotFoundException 指定类不存在
- NumberFormatException 字符串转换为数字异常
- IndexOutOfBoundsException 数组下标越界异常
- ClassCastException 数据类型转换异常
- FileNotFoundException 文件未找到异常
- NoSuchMethodException 方法不存在异常
- IOException IO 异常
- SocketException Socket 异常
6、多线程
# Java中的线程状态?
1. 初始状态(NEW)
实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态,线程已构建还没有调用 start()
方法。
2.运行(RUNNABLE)
- Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
- 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。
- 就绪状态(ready)的线程在获得CPU时间片后变为运行中状态(running)
3.阻塞状态(BLOCKED)
阻塞状态是线程(阻塞在进入synchronized关键字修饰的方法或代码块)获取锁时的状态。
4.等待状态(WAITING)
处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
5.超时等待状态(TIMED_WAITING)
处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
6.终止状态(TERMINATED)
当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为该线程终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
# Java为什么要将notify/notifyAll和wait作为Object类的方法,而不是Thread的方法?
notify/notifyAll和wait主要是实现java线程之间的通信机制。java提供的锁都是对象级的,而不是线程级的。
- 首先引出问题,因为多线程在执行时,会遇到一些问题,产生问题的关键就是在共享资源的更新操作上容易产生冲突;
- 解决冲突的方式则是从共享资源的占用机制入手,保证共享资源同一时刻只能被一个线程占用,从而达到数据一致。
- 具体实现方式中的一种,在Java中提供了 synchorinzed关键字 ,在该关键字修饰的代码内,称为同步代码块,线程执行该区域代码需要获取锁,在获取成功之后,其他线程需要等该线程执行完毕释放锁之后才能获取到。
- 在同步代码块中,可以使用 notify/notifyAll和wait 来控制当前占用资源的线程进入阻塞队列与唤醒进入就绪队列,也就是说,上述两个方法实际上实现的是线程之间的通信机制,用来通知线程的阻塞与唤醒。
- 引出最终问题,为什么notify/notifyAll和wait 定义在Object中而不是Thread类?
- 线程为了进入临界区(也就是同步块内),线程需要获得锁并等待锁可用。它们并不知道也不需要知道哪些线程持有锁并要求释放锁,只需要知道锁是由某一个线程保持的,它们应该等待锁。所以锁的持有状态应该由同步监视器来获取,而不是线程本身。所以适合等待和通知在Object类而不是Java中的线程。
- 如果不能通过类似
synchronized
这样的Java关键字来实现这种机制,那么Object类中就是定义它们最好的地方,以此来使任何Java对象都可以拥有实现线程通信机制的能力。
7、文件与I/O流
# Java中的I/O流分类
- 按照流的流向分为输入流和输出流
- 按照操作单元分为字节流和字符流
- 按照流的角色分为节点流和处理流
# 既然有了字节流,为什么还要有字符流呢?
不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,为什么I/O流操作要分为字节流和字符流呢?
- 字符流是有Java虚拟机将字节转换得到的,该过程是非常耗时的,并且,如果不知道编码类型就很容易出现乱码的问题。所以,I/O流就干脆提供流一个直接操作字符的接口,方便我们平时对字符流进行操作。
# BIO、NIO、AIO有什么区别
BIO(Blocking I/O)
:同步阻塞模式,数据的读取写入必须阻塞在一个线程内等待其完成。NIO(Non-Blocking I/O)
:同步非阻塞模型。支持面向缓冲的,基于通道的操作方法。AIO(Asynchronous I/O)
:NIO ß的改进版,异步非阻塞模型。基于事件和回调机制实现的,也就是操作之后会直接返回,不会阻塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
# BeanFactory和FactoryBean的区别?
- BeanFactory是接口,提供了IOC容器最基本的形式,给具体的IOC容器的实现提供了规范,
- FactoryBean也是接口,为IOC容器中Bean的实现提供了更加灵活的方式,FactoryBean在IOC容器的基础上给Bean的实现加上了一个简单工厂模式和装饰器模式
- 区别:
- BeanFactory是个Factory,也就是IOC容器或对象工厂,生产和管理Bean
- FactoryBean是个Bean,但这个Bean不是简单的Bean,而是一个能生产或者修饰对象生成的工厂Bean,它的实现与设计模式中的工厂模式和修饰器模式类似。
- 在Spring中,所有的Bean都是由BeanFactory(也就是IOC容器或对象工厂)来进行管理的。
**链接:**https://mp.weixin.qq.com/s?__biz=MzAwMjk5Mjk3Mw==&mid=2247499324&idx=2&sn=a4139cdd99ca59ba6380eaf320064b80&chksm=9ac3401eadb4c908fc751c885ec18cbdd682222516a297a854c756de58531f6bd5093c31567f&scene=27
**一般情况下,Spring通过反射机制利用<bean>
的class属性指定实现类实例化Bean,在某些情况下,实例化Bean过程比较复杂,如果按照传统的方式,则需要在<bean>
中提供大量的配置信息。配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。**Spring为此提供了一个org.springframework.bean.factory.FactoryBean的工厂类接口,用户可以通过实现该接口定制实例化Bean的逻辑。FactoryBean接口对于Spring框架来说占用重要的地位,Spring自身就提供了70多个FactoryBean的实现。它们隐藏了实例化一些复杂Bean的细节,给上层应用带来了便利。从Spring3.0开始,FactoryBean开始支持泛型,即接口声明改为FactoryBean<T>
的形式