JVM
、JDK
和JRE
的区别
JVM
(java
虚拟机)是运行java
字节码的虚拟机,不同的系统有特定的 JVM
实现,比如常见的 HotSpot VM
就是 JVM
的一种实现,除此之外还有 J9 VM
(常用于 IBM
硬件平台)等。
JRE
是 java
运行时环境,包括 JVM
、java
库、java
命令和其他一些基础构件,但是 JRE
不能用于创建新程序。
JDK
是功能齐全的 java SDK
,包括 JRE
、编译器(javac
)和一些工具(如 javadoc
和jdb
,能够创建和编译程序。
静态方法为什么不能调用非静态成员?
静态方法在类加载的时候就会分配内存,可以通过类名去访问,而非静态成员只有在对象实例化之后才存在,且需要通过类的实例对象去访问。因此在非静态成员不存在的时候,静态方法就已经存在了,此时静态方法调用还不存在的非静态成员是非法的。
重载与重写的区别
重载:相同的函数名,参数列表必须不同(返回值类型可以不同)(发生在编译器)
重写:方法名、参数列表必须相同(子类方法的返回值类型应比父类方法返回值类型更小或者相等,如果父类方法返回值类型是 void
或者基本数据类型则子类方法返回值类型不可更改;抛出的异常范围小于等于父类;访问修饰符范围大于等于父类)(private
/ final
/ static
修饰的方法不能被重写,构造方法无法被重写可以重载)(发生在运行期)
==
和equals()
的区别
==
对于基本数据类型来说,比较的是变量的值;对于引用数据类型来说比较的是对象的内存地址。
equals()
不能用来判断基本数据类型变量,只能用于判断两个对象是否相等。如果没有重写equals()
则 equals()
相当于==
,比较的是对象的内存地址,如果重写了 equals()
则可以比较对象的属性,即属性相等则认为两个对象相等。
为什么重写 equals()
必须重写 hashCode()
两个对象的 hashCode
相同,并且equals()
返回 True
,我们才认为这两个对象是相等。equals()
返回 True
而 hashCode
并不一定相等,因此需要重写 hashCode()
。
而且在 HashMap
中,判断是否有重复元素时,是先通过 hashCode
计算元素位置,再通过 equals()
来判断元素是否相等的。
java
中的基本数据类型
byte
、 short
、 int
、 long
、 float
、 double
、char
、 boolean
基本数据类型的线程安全
8种基本数据类型中 long
类型和 double
类型是64位的,因此在32位的系统下读写操作是分两次的,因此不是线程安全的。如果硬件,操作系统,JVM
都是64位的,则单次可以操作64位的数据,读写应该是原子性的。
包装类型的常量池技术
Byte
, Short
, Integer
, Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据,Boolean
直接返回 True
or False
。两种浮点类型的包装类 Float
和 Double
没有实现常量池技术。
自动装箱与拆箱
装箱:将基本类型用对应的引用类型包装起来
Integer i=10;//等价于Integer i=Integer.valueOf(10);
拆箱:将包装类型转换为基本数据类型
int n=i;//等价于int n=i.intValue();
在进行比较前会自动拆箱。
class AutoUnboxingTest {
public static void main(String[] args) {
Integer a = new Integer(3);
Integer b = 3; // 将3自动装箱成Integer类型
int c = 3;
System.out.println(a == b); // false 两个引用没有引用同一对象
System.out.println(a == c); // true a自动拆箱成int类型再和c比较
}
}
面向对象三大特征
封装:封装是指把一个对象的状态信息隐藏在对象内部,不允许外界直接访问,只提供一些可以被外界访问的方法来操作对象属性。
继承:用已经存在的类作为基础来创建新类,新类可以增加新的属性和方法,也可以用父类的功能,但不能选择性地继承父类。继承提高了代码的可重用性和可维护性,节省了创建新类的时间,提高了开发效率。
多态:多态具体表现为父类引用指向子类实例。
对象类型和引用类型之间具有继承(类)/ 实现(接口)的关系;
引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期才能确定;
多态不能调用只在子类存在但在父类不存在的方法;
如果子类重写了父类的方法,真正执行的是子类的方法,否则执行的是父类的方法。
java
中的权限修饰符
修饰符修饰方法或属性:
public:允许跨类和跨包访问
default:只允许在同一个包中跨类
protected:只允许在类或者子类中访问,子类可以在不同包中
private:只允许在本类中访问
注意:protected和private不能用来修饰一般的类,否则会报编译器不允许的错误,内部类除外。
补充:想要这个类的属性和方法可以被任何子类继承,我就用protected。想要这个类的属性和方法不能被任何子类继承,我就用private。同理,想要这个类被继承,我就用abstract。我不想这个类被继承,就用final。
https://www.cnblogs.com/AleiCui/p/12792565.html
接口和抽象类的区别
1、接口的意义在于可以对行为进行抽象,而抽象类是对事物进行抽象;一个类实现了接口,则说明这个类拥有了这个接口中的所有行为,如果接口中改变了行为,则这个类也要改变。如果一个类继承了抽象类,则这个类就可以使用抽象类中的方法,并可以重新去实现抽象类中的抽象方法,如果抽象类中改变了非抽象方法,这个类不用做其他改变。
2、抽象类中可以有静态变量,实例变量,而接口中必须是final类型修饰的静态变量
3、抽象类中可以抽象方法也可以有非抽象方法,接口中只能有抽象方法
4、抽象类中有构造函数,而接口中没有构造函数
5、抽象类中可以有代码块,接口中不能有代码块
6、抽象类只能被单继承,接口可以被多实现。
注意:虽然抽象类中有构造函数,但是抽象类是不能直接实例化的。只有在子类继承了这个抽象类之后,子类实例化的时候,才会调用(父类)抽象类中的构造函数。
深拷贝和浅拷贝
浅拷贝:复制一个对象时,创建了一个新的对象,对于原对象里的基本数据类型,将其值拷贝进来,对于原对象里的引用数据类型,将其引用拷贝进来。这样如果原对象更改了引用对象,会对拷贝对象产生影响。
深拷贝:对于原对象里的引用对象,不是拷贝引用,而是创建一个新的对象,这样原对象更改了引用对象,也不会对拷贝对象产生影响。
引用拷贝:两个不同的引用指向同一个对象
通过实现 cloneable
接口,重写 clone()
方法来实现浅拷贝和深拷贝。
实现深拷贝的两种方式
1)实现 Cloneable
接口,重写 Object
类中的 clone()
,通过层层克隆来实现深拷贝。
2)通过序列化(都要实现 Serializable
接口)的方法,将对象写入到流中,再从流中读取出来。这种方式虽然效率低下,但是可以实现真正意义上的深度克隆。
https://juejin.cn/post/6982209049416859678 "通过序列化和反序列化实现深拷贝"
object
类常见的方法有哪些
public final native Class> getClass()//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。
public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。
public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。
protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。
public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。
public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。
public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。
public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作
String
、 StringBuilder
、 StringBuffer
的区别
可变性:String
是不可变的。String
类中保存字符串的数组被 final
修饰且为私有,并且并没有向外部提供修改这个字符数组的方法;且 String
类被final
修饰,使其不能被继承,进而避免了子类破坏 String
。因此,当String
对象发生改变时,都会重新生成一个新的 String
对象,然后将引用指向新的 String
对象。(对于 final
修饰的引用类型,其指向的内存地址不可变,但是对象内容是可以发生变化的)
StringBuilder
和 StringBuffer
也是使用字符数组来保存字符串,但是没有使用 final
和 private
关键字修饰,且还提供了修改字符串的方法,是可变的。
线程安全性:String
对象是不可变的,可视为常量,因此是线程安全的。StringBuffer
对方法加了同步锁,也是线程安全的。而 StringBuilder
没有对方法加锁,是非线程安全的。
为什么要将String设置成不可变
1、不可变的对象可以当做散列表的key
2、不可变的对象本身就是线程安全的,不需要额外进行同步
反射会改变对象的不可变性。
String
的 hashCode
是怎么生成的
https://www.huaweicloud.com/articles/e6ae1a928b6bea00bc83bd481cafa336.html
字符串常量池
字符串常量池是 JVM
为了提升性能和减少内存消耗针对 String
类专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
tring aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
System.out.println(aa==bb);// true
JDK1.7
之前运行时常量池逻辑上包含字符串常量池,放在方法区中。JDK1.7
时字符串常量池被放到了堆中。
泛型
泛型是 JDK5
中引入的一个新特性,泛型提供了编译时类型安全检查机制,该机制允许编译时检测到非法类型,其本质是参数化类型,即将所操作的数据类型指定为一个参数。
但是,在需要转型的时候,编译器会根据所操作数据的类型自动实行安全地强制转型,所以在运行期,所有的泛型信息都会被擦除,即常说的泛型擦除。
泛形要求能包容的是引用类型,而基本类型在 java
里不属于对象。所以泛型不能是基本数据类型。(所有对象都继承了 Object
类型)
(有泛型类、泛型接口、泛型方法)
反射
反射可以让我们在运行期分析类以及执行类中的方法,通过反射可以获取任意一个类的所有属性和方法,并可以调用。这也使得反射引入了安全问题,比如可以无视泛型参数的安全检查。
反射的应用场景
Spring
、Spring Boot
、MyBatis
等框架都大量使用了反射机载。
动态代理和注解也是基于反射实现的。
注解
注解可以用于修饰类、方法或者属性,其本质是一个继承了 Annotation
的特殊接口。
注解只有被解析后才会生效,常见的解析方式有两种:
编译期直接扫描:编译器在编译代码时扫描对应的注解并处理,比如某个方法使用 @Override
注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。
运行期通过反射处理:框架中自带的注解都是通过反射处理的。
异常
java
异常类的层次结构:
Exception
:程序本身可以处理的异常,可以通过 catch
来进行捕获。Exception
又可以分为 Checked Exception
(受检查异常,必须处理否则无法通过编译)和 Unchecked Exception
(非受检查异常,可以不处理,RuntimeException
及其子类都是非受检异常)。
Error
:程序无法处理的错误,没办法通过 catch
来进行捕获,发生错误时,JVM
一般会选择线程终止。
try-catch-finally
的使用及注意事项
try
块: 用于捕获异常。其后可接零个或多个 catch
块,如果没有 catch
块,则必须跟一个 finally
块。
catch
块: 用于处理 try 捕获到的异常。
finally
块: 无论是否捕获或处理异常,finally
块里的语句都会被执行。当在 try
块或 catch
块中遇到 return
语句时,finally
语句块将在方法返回之前被执行,且返回值在 finally
块执行之前就已经确定了,不会因为 finally
块的执行而发生改变。
注意:不要在 finally 语句块中使用 return! 当 try
语句和 finally
语句中都有 return
语句时,try
语句块中的 return
语句不会被执行。
finally
中的代码一定会执行吗
在 finally
之前虚拟机被终止运行的话(或者线程死亡、CPU关闭),finally
中的代码就不会被执行。
听说过 try-with-resources
吗
在面对必须要关闭的资源时,应该优先考虑使用 try-with-resources
,可以让我们更容易编写必须要关闭的资源的代码。
序列化与反序列化(串行化与并行化)
序列化:将对象(实例化后的 Class
类)转换为字节序列(二进制字节流),序列化后可以持久化到磁盘上,也可以在网络上进行传输。
反序列化:将字节序列恢复成对象。
https://juejin.cn/post/6844904176263102472
实现了Serializable
接口的类都是可以被序列化的!
- 凡是被
static
修饰的字段是不会被序列化的 - 凡是被
transient
修饰符修饰的字段也是不会被序列化的(transient
只能用于修饰字段)
如果在序列化某个对象时,不希望某个字段被序列化(比如这个字段存放的是隐私值,如:密码
等),那这时就可以用transient
修饰符来修饰该字段。这样在反序列化时,这个字段就会为被置成默认值而非真实值。
java
中的 IO
流
按照流向可以分为输入流和输出流;
按照操作单元可以分为字符流和字节流;
信息的最小存储单元是字节,为什么需要有字符流操作
字符是由字节转换得到的,转换过程是比较耗时的,且如果不知道编码类型就很容易出现乱码问题,所以 I/O
流就提供了一个直接操作字符的接口,方便对字符进行流操作。
如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
内部类
内部类是一个编译期的概念,一旦编译成功,就会成为完全不同的两类。
https://www.jianshu.com/p/a9467e690eb0
1、由于内部类可以直接访问外部类中的所有变量,因此我们可以通过内部类来访问外部类中的私有属性和方法。(闭包)
2、外部类对内部类的封装主要表现为,在其它地方访问内部类,受到了外部类的限制。
3、使用内部类可以间接地进行多重继承。
内部类变量的可见性
1)静态内部类:创建在类中,和成员变量同级,带有static关键字
外部类和静态内部类没有访问限制。
2)成员内部类:创建在类中,和成员变量同级
外部类不能直接使用内部类中的成员变量,需要创建出内部类对象后再访问,而创建内部类对象需要用外部类对象来创建。
内部类可以直接访问外部类中的成员变量和静态变量,内部类中不能有静态变量。
3)局部内部类:创建在方法内部
外部类不能访问内部类中的变量
内部类可以直接访问外部类中成员变量和静态变量,不能含有静态变量
4)匿名内部类:建立在方法内部,且没有名称
场景:只用到匿名内部类的一个实例,且在类定义后马上就使用到
不能有构造方法和静态成员、静态方法,局部内部类中的规则也作用与匿名内部类。