Java基础
语法基础
a=a+b 和 a+=b
+=操作隐式的将操作的结果类型强制转换成持有结果的类型,而+不会
比如对byte,short,int类型的操作,会先将他们提升到int类型,然后在执行操作。所以比如我定义了两个byte类型的a和b,值均为127,如果使用b=a+b则会报错,说int不能转为byte,而使用+=会将其强制转换成int,也就不会报错。
3*0.1 == 0.3 将会返回什么? true 还是 false?
结果肯定是返回false了,因为有些浮点数不能完全的表示出来,只能是在误差允许的范围内接近真实值。
能在 Switch 中使用 String 吗?
java5之前只允许使用byte int short char,在java5又添加了一个枚举类型,在java7之后允许使用String。
final、finalize 和 finally 的不同之处?
final 是一个修饰符,可以修饰变量、方法和类。对于final 修饰变量,该变量的值在初始化后不能被改变。
finalize() 方法用于垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。
finally 是一个关键字,与 try 和 catch 一起用于异常的处理。finally 块一定会被执行,无论在 try 块中是否有发生异常。
String、StringBuffer与StringBuilder的区别?
三者的底层都是基于char数组来实现对字符串的操作的。
String是不可变类,一旦被创建,其值就不能被更改。而我们常见的对String的操作实际上都是创建了一个新的String对象。因此,当有大量对字符串的频繁操作时,尽量不要使用String类型,以免产生较多的临时变量造成内存和性能的损失。
StringBuffer和StringBuilder都是可变的类,对其操作都是基于对象本身的操作。
在线程安全方面,由于String是不可变的,所以对于多个线程同时访问同一个String不会产生线程安全问题。StringBuffer因为他的所有公共方法都是使用synchronized关键字修饰的,所以在多线程访问时,synchronized关键字会锁定整个对象以确保线程安全。StringBuilder是线程不安全的,因为他的底层没有使用线程安全的机制。
相对而言,由于StringBuffer使用时需要加锁,而StringBuilder不需要加锁,所以在不涉及线程安全的场景下可以使用效率更好的StringBuilder
Java移位运算符?
使用移位运算符替代乘除能够提升运行效率。
-
:带符号右移,x >> 1,相当于x除以2,正数高位补0,负数高位补1
- <<:左移运算符,x << 1,相当于x乘以2(不溢出的情况下),低位补0
-
:无符号右移,忽略符号位,空位都以0补齐
由于 double,float 在二进制中的表现比较特殊,因此不能来进行移位操作。
移位操作符实际上支持的类型只有int和long,编译器在对short、byte、char类型进行移位前,都会将其转换为int类型再操作。
当 int 类型左移/右移位数大于等于 32 位操作时,会先求余(%)后再进行左移/右移操作。也就是说左移/右移 32 位相当于不进行移位操作(32%32=0),左移/右移 42 位相当于左移/右移 10 位(42%32=10)。当 long 类型进行左移/右移操作时,由于 long 对应的二进制是 64 位,因此求余操作的基数也变成了 64。
Java中的访问类型?
- public:公共的,最宽松的访问级别。可以被任何其他类访问
- protected:受保护的,比公共级别更严格一些。使用protected修饰的成员只能在当前类、当前包内或当前类的子类中被访问。
- default:使用default修饰的成员只能在同一包内被访问,不能被其他包中的类访问。
- private:私有的,最严格的访问级别。使用private修饰的成员只能在当前类中被访问,其他任何类(包括同一包内的类和子类)都不能访问。
JDK、JRE、JVM 三者之间的关系?
JDK:是Java开发工具包,包括了Java运行环境JRE,Java工具以及Java基础类库
JRE:是Java运行环境,包含了JVM标准实现以及Java核心类库
JVM:Java虚拟机,是Java实现跨平台的核心部分,Java文件会首先被编译成class类文件,然后可以将其放在虚拟机上执行。
Java 中创建对象的几种方式?
- 使用new关键字
- 使用Class类的newInstance方法(Class.forName.newInstance();)
- 使用clone方法
Integer 和 int 的区别?
Integer是包装类,需要实例化后使用,默认值是null,而int是Java八大基本数据类型之一,默认值是0.
对于两个通过new生成的Integer对象是不相等的,因为他们的内存地址不同。
但是如果一个是Integer类型,一个是int类型,只要他们的值相等,得到的就是true,因为Integer在和int比较时会自动拆箱成int类型,实际上是两个int在比较
非new生成的Integer和new生成的integer在比较时结果也是false,因为一个指向常量池,一个指向堆区
两个非new定义的Integer类型的变量,如果其取值在-128到127之间结果返回true,否则返回false,因为在-128到127之间的数会进行缓存,第二次定义的时候直接从缓存中取出。在这个区间外的就需要new了
装箱和拆箱
装箱是将基本类型用它们对应的引用类型包装起来;比如 Integer i= 10,实际上就是调用了包装类的valueOf()方法。
拆箱是将包装类型转换为基本数据类型;比如int n = i,实际上就是调用了intValue()方法
基本类型和包装类型的区别?
包装类型可以用于泛型,但是基本类型不可以。包装类型他的大部分对象都是保存在堆区的,但是有少部分是保存在常量池的(--包装类缓存机制)
基本数据类型有默认值,而包装类不赋值就是null。包装类建议使用equals比较对象的值是否相等,如果使用比较会比较二者的地址是否相等。基本数据类型可以使用进行比较。
包装类型的缓存机制?
Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False。
如果我们使用的装箱的方式创建值为缓存范围内的对象,他会返回缓存区的对应的对象,比如Integer i = 10,就会返回缓存区中的对象,而Integer i = 1000就会在堆区创建对象。
为什么说 Java 语言"编译与解释并存"?
这是因为 Java 语言既具有编译型语言的特征,也具有解释型语言的特征。对于一个Java文件,首先需要经过编译生成字节码文件,也就是.class文件,这个就是编译的过程,然后将生成的字节码文件输入到解释器来解释执行,这就是解释的过程。
Java 和 C++ 的区别?
首先他们都是面向对象的语言,都有面向对象的三大特征。但是Java中没有指针来访问内存,程序内存更加安全,并且Java提供垃圾回收机制,不需要手动释放内存。Java不支持多重继承,只支持单继承,而C++支持多继承。
注释有哪几种形式?
- 单行注释:通常用于解释方法内某单行代码的作用。 //
- 多行注释:通常用于解释一段代码的作用。 /*
- 文档注释:通常用于生成 Java 开发文档。/** */
标识符和关键字的区别是什么?
通俗的讲,标识符就是一个名字,比如我们定义类时起的类名,定义方法是起的方法名等等都是标识符。关键字可以理解为一类特殊的标识符,它是被赋予特殊含义的,比如public private,new,static等都是关键字
continue、break 和 return 的区别是什么?
continue:指跳出当前的这一次循环,继续下一次循环。
break:指跳出整个循环体,继续执行循环下面的语句。
return: 用于跳出所在方法,结束该方法的运行。
Java 中的几种基本数据类型?
- 6 种数字类型:
- 4 种整数型:byte、short、int、long
- 2 种浮点型:float、double
- 1 种字符类型:char
- 1 种布尔型:boolean。
| 基本类型 | 位数 | 字节 | 默认值 | 取值范围 |
| --- | --- | --- | --- | --- |
| byte | 8 | 1 | 0 | -128 ~ 127 |
| short | 16 | 2 | 0 | -32768(-2^15) ~ 32767(2^15 - 1) |
| int | 32 | 4 | 0 | -2147483648 ~ 2147483647 |
| long | 64 | 8 | 0L | -9223372036854775808(-2^63) ~ 9223372036854775807(2^63 -1) |
| char | 16 | 2 | 'u0000' | 0 ~ 65535(2^16 - 1) |
| float | 32 | 4 | 0f | 1.4E-45 ~ 3.4028235E38 |
| double | 64 | 8 | 0d | 4.9E-324 ~ 1.7976931348623157E308 |
| boolean | 1 | 1 | false | true、false |
boolean:理论上只占用1位,但是计算机处理数据的最小单位是1个字节,所以需要将其他位补0,也就是说此时boolean看上去占用1字节。但是《Java虚拟机规范》中指出,在编译后会将boolean变量转换成int变量也就是占用四个字节,而boolean数组会转换成byte数组,每个元素占1字节。
将boolean转换成int类型的原因是,对于当下32位的处理器(CPU)来说,一次处理数据是32位,所以说他使用 4 个字节是最为节省的,哪怕你是 1 个 bit 他也是占用 4 个字节。因为 CPU 寻址系统只能 32 位 32 位地寻址,具有高效存取的特点。
为什么浮点数运算的时候会有精度丢失的风险?
计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。比如0.2,他在计算的时候就出现了无限循环,所以无法精确的表示。
如何解决浮点数运算的精度丢失问题?
可以使用 BigDecimal 来解决。 BigDecimal底层使用了BigInteger(底层是动态数组按需分配内存空间,理论上内存只要足够大,就能计算无穷的整数),用来记录浮点数转换成整数的值,并且同时使用scale来记录小数点的位置。由于十进制整数在转化为二进制数时不会有精度问题,所以他的原理就是将浮点数运算转换成整数运算,最后再加上小数点。
超过 long 整型的数据应该如何表示?
可以使用BigInteger,他的底层是动态数组按需分配内存空间,理论上内存只要足够大,就能计算无穷的整数
变量
静态变量和实例变量的区别?
静态变量是被static修饰的变量,也称为类变量,它属于类。因此不管创建多少个对象,静态变量在内存中有且仅有一个。
而实例变量属于某一实例,需要先创建对象通过对象才能进行访问。
成员变量与局部变量的区别?
从语法角度看,成员变量是属于类的,可以被public,static等修饰符修饰,而局部变量是属于代码块或方法中的参数,不能被public static等修饰符修饰。
从存储角度看,成员变量如果使用了static修饰,那么这个变量属于类,如果没有static 修饰则属于实例对象的。对象存储在堆区,局部变量存在于栈区
从生存时间角度看,成员变量是对象的一部分,随着对象的创建而存在,对象的删除而消亡,局部变量随着方法的调用而生成,方法调用的结束而消亡。
字符型常量和字符串常量的区别?
字符型常量是使用单引号定义的一个字符,每个字符都应该有一个对应的ASCII的值,它占用两个字节,使用char定义。
字符串常量是使用双引号定义的若干字符,他的长度不固定,字符串常量代表该字符串在内存中存放的位置。
方法
this() & super()在构造方法中的区别?
this调用的是同一类中的其他构造方法,而super调用的是其父类的构造方法,super和this都需要放在构造器的第一行,并且二者不能同时出现,否则编译会不通过。二者指代的都是对象,所以不能再static的环境中使用。
重载和重写的区别?
重载就是所谓的编译时多态,是指同一个类中包含名字相同,参数不同的方法,在编译时可以通过方法签名来区分调用的是哪个方法。
重写可以理解为运行时多态的一环,是指子类继承父类后,对父类的某些方法重新实现。
Java 中是否可以重写一个 private 或者 static 方法?
首先是static方法,static方法不能被覆盖,因为重写是基于运行时动态绑定的,而static方法是编译时静态绑定的。static方法和类的实例是不相关的,所以不能重写
private也不能重写,因为private修饰的方法和变量只能在当前类访问,子类继承当前类也无法访问到private变量或方法,也就算不上重写。
构造方法有哪些特性?
名字与类名相同,没有返回值,创建对象时自动执行。可以被重载,不能被重写。
定义一个不做事且没有参数的构造方法有什么作用?
在Java中,创建子类对象时,如果没有用 super() 来调用父类特定的构造方法,则会调用父类中"无参构造方法"。如果此时父类没有无参构造,则编译报错。解决方案是要么给子类加上super指定父类的具体构造函数,要么给父类加上无参构造函数。
什么是方法的返回值?方法有哪几种类型?
方法的返回值:是指我们想要得到某个方法体中的代码执行后产生的结果!
- 无参无返回值
- 有参无返回值
- 有返回值有参
- 有返回值无参
静态方法为什么不能调用非静态成员?
静态方法是属于类的,在类加载的时候就会分配内存。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
静态方法和实例方法有何不同?
1、调用方式:在外部调用静态方法时,可以使用 类名.方法名 的方式,也可以使用 对象.方法名 的方式,而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象 。
2、静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。
什么是可变长参数?
从 Java5 开始,Java 支持定义可变长参数,就是允许在调用方法时传入不定长度的参数,定义方法就是在参数类型后面加三个点。可变参数只能作为函数的最后一个参数。遇到方法重载时,会优先匹配固定参数的方法,因为固定参数的方法匹配度更高。
//TODO Java 中的参数传递时传值呢?还是传引用?
//TODO Java 对象的大小是怎么计算的?
https://www.iamshuaidi.com/1165.html
面向对象
面向对象的特征
面向对象分为三个特性,封装继承和多态
首先是封装,封装是指讲数据和对数据的操作封装到一个抽象数据类型中,使其成为一个不可分割的整体。通常我们成为类,数据被保护在类中,对外通过定义的接口进行操作。
封装有几大优点,比如能够减少代码的耦合性,便于维护,能够增加软件的可重用性等等。
继承实际上就是is-a的关系,比如我定义了一个Animal类,又定义了一个Cat类,如果让cat类继承animal类,那么这个cat就会继承animal类的非private属性和方法。
实际上不能继承private修饰的属性和方法是从结果出发的,通过编译器打断点可以发现,实际上子类能够继承private属性,而private修饰的属性限制了访问方式只能在类的内部,仅是一个访问控制,所以从结果上看,private好像不能被继承,但实际上是继承了,但是无法访问。
多态分为两种,编译时多态和运行时多态。编译时多态主要指的是方法的重载,比如说我在一个类里面定义了两个名字相同的方法,他们的参数不同,在编译阶段可以根据方法签名的不同来选择正确的方法实现。运行时多态有三个条件,继承,重写,向上转型。比如在定义时,我定义了一个父类的引用,指向了子类对象,并且子类对象重写了父类的方法,我在使用的时候,如果调用这个父类的方法,则实际调用的是子类重写的那个方法。
面向对象主要是以对象为编程核心,会将问题抽象成对象,然后用对象方法解决问题。面向过程主要是以函数开发为主,将问题拆分成一个一个的函数然后依次执行。
对象的相等和引用相等的区别
- 对象相等一般比较的是在内存中存储的两个对象的内容是否相等。一般使用equals
- 引用相等一般比较的是他们指向的内存是否相等。一般使用==
一个类没有声明构造方法,该程序能正确执行吗?
可以执行,如果我们没有显示的声明构造方法,Java编译器则会自动为该类生成一个默认的无参构造方法,这个生成的默认方法会在创建对象时自动调用。
接口与抽象类的区别?
相同点:
- 都不能被实例化。
- 都可以包含抽象方法。
- 都可以有默认实现的方法(Java 8 可以用 default 关键字在接口中定义默认方法)。
区别:
- 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
- 一个类只能继承一个抽象类,但是可以实现多个接口。
- 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。
深拷贝和浅拷贝区别了解吗?什么是引用拷贝?
- 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
- 深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
- 引用拷贝 :简单来说,引用拷贝就是两个不同的引用指向同一个对象。
//TODO 对象的访问定位的两种方式?
https://www.iamshuaidi.com/1168.html
Object
常见的方法
java
public final native Class<?> getClass()
public native int hashCode()
public boolean equals(Object obj)
protected native Object clone() throws CloneNotSupportedException
public String toString()
public final native void notify()
public final native void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException
public final void wait() throws InterruptedException
protected void finalize() throws Throwable { }
- getClass:用于返回当前运行时对象的 Class 对象
- hashCode:用于返回对象的哈希码,主要使用在哈希表中
- equals:用于比较 2 个对象的内存地址是否相等
- clone:用于创建并返回当前对象的一份拷贝。(浅拷贝)
- toString:返回类的名字实例的哈希码的 16 进制的字符串。
- notify:唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
- notifyAll:跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
- wait(long timeout):暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
- wait(long timeout, int nanos):多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。
- wait():跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
- finalize():实例被垃圾回收器回收的时候触发的操作
== 和 equals() 的区别
- 对于基本数据类型来说,== 比较的是值,equals()不能作用于基本数据类型,但是其包装类可以使用equals方法来比较两个对象的值是否相等。
- 对于引用数据类型来说,== 比较的是对象的内存地址。对象的比较可以使用equals方法,但是默认的实现是进行地址的比较。可以对equals方法重写来比较其对象属性是否相等。
为什么要有 hashCode?
hashCode方法计算哈希码,一方面是判断对象是否相等,一方面是提升一些容器的效率。
在我们使用HashMap,HashSet等容器的时候,通过计算哈希码可以以O(1)的时间复杂度来访问数据,从而避免每个元素进行对比所浪费的时间。当我们想要插入新的数据的时候,首先会计算哈希码,确认当前元素所处的索引位置。此时hashCode的方法也起到了判断对象是否相等的作用。只要哈希函数选的足够好,发生哈希碰撞的概率就会很小。当发生哈希碰撞时,再根据equals方法进一步判断两个对象是否相等。
两个对象哈希码相同,但是他们不一定是相同的对象,只有当两个对象hashCode方法结果都相同,equals方法返回true时才认为相同;但是两个相同的对象一定会有相同的哈希码。
为什么重写 equals() 时必须重写 hashCode() 方法?
主要是为了符合以散列存储为核心的集合的规范。因为在散列集合判断对象是否相等时,首先会计算两个对象的hashCode是否相等,如果不相等则两个对象一定不相等,如果相等,考虑到哈希碰撞的可能性,还需进一步判断,此时就用到了equals方法。如果不对equals方法重写,object类默认实现是比较两个对象地址是否相等。而在大部分场景下,我们需要判断的是对象的属性是否相等,如果相等则认为两个对象是相等的,而不是判断其地址。所以重写equals方法来构建实际的、判断对象是否相等的逻辑。
String
String、StringBuffer、StringBuilder 的区别?
在可变性方面,String是不可变的,而StringBuilder和StringBuffer都是可变的,他们都继承了AbstractStringBuilder类,在这个父类中使用字符数组保存字符串,并提供了许多对字符串操作的方法如append等。
在线程安全方面,由于String是不可变的,所以对于多个线程同时访问同一个String不会产生线程安全问题。StringBuffer因为他的所有公共方法都是使用synchronized关键字修饰的,所以在多线程访问时,synchronized关键字会锁定整个对象以确保线程安全。StringBuilder是线程不安全的,因为他的底层没有使用线程安全的机制。
性能方面,由于对String的每次操作都是产生了一个新的String 对象,所以在有对字符串较多的修改的操作时,尽量避免使用String,以免产生过多的中间变量从而造成内存上的损失和性能下降。而StringBuffer相较于StringBuilder多一个加锁的过程,所以会造成性能上的损失。所以在单线程的场景下可以使用StringBuilder来提升效率。
String 为什么是不可变的?
String 类用被 final 关键字修饰字符数组来保存字符串,所以String不可变。
但是实际上这并不是String不可修改的根本原因。我们知道被final修饰的类不能被继承,修饰的基本数据类型变量不能修改其值,修饰的引用类型变量不能修改其指向其他对象,修饰的方法不能重写。而虽然String类中保存字符串的char数组被final修饰,但是这个数组虽然不能修改其引用,但是可以修改其内容。
所以事实上String之所以不能修改,一方面是因为final修饰的char数组,我们不能修改其引用,同时String并未对这个private修饰的变量提供修改的接口,所以我们没有办法修改他的值。
字符串拼接用"+" 还是 StringBuilder?
String类型的变量也可以使用+来进行字符串的拼接,但是通过对编译后的字节码查看可以发现,String变量使用+拼接时,实际上是编译器临时创建了一个StringBuilder对象,然后调用StringBuilder类的append方法来进行字符串的拼接,拼接完调用toString方法将其转换成字符串。
考虑一种情况,当在循环中使用+拼接字符串时,编译器并不会创建一个StringBuilder重复利用,而是每次调用都重新创建。所以为了避免这种情况对资源的浪费,尽量使用StringBuilder进行字符串的拼接。
实际上在Java9版本改进了+带来的产生大量临时对象的问题,它使用动态方法进行字符串的拼接。
String s1 = new String("abc");这句话创建了几个字符串对象?
1个或2个。
如果字符串常量池中不存在字符串对象"abc"的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中。
如果字符串常量池中已存在字符串对象"abc"的引用,则只会在堆中创建 1 个字符串对象"abc"。
String#intern 方法有什么作用?
String.intern() 是一个 native(本地)方法,其作用是将指定的字符串对象(新对象或源对象的引用)保存在字符串常量池中,可以简单分为两种情况:
- 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
- 如果字符串常量池中没有保存了对应的字符串对象的引用,在jdk1.7之前,如果常量池没有值等于该字符串的值,那么会创建一个新的副本,将这个副本的引用保存在字符串常量池中,也就是说会新创建一个对象。而1.7版本及之后会直接将该对象的引用保存在字符串常量池。
什么情况下调用intern方法会在常量池新保存一个引用?动态生成的字符串例子
java
String part1 = "java"; // 假设常量池中已存在
String part2 = "intern"; // 假设常量池中已存在
String combined = part1 + part2; // "javaintern", 动态生成,假设之前没有以字面量或通过intern加入常量池
String internedCombined = combined.intern(); // 这将把"javaintern"添加到常量池中
分析创建了多少个对象```java
String s1 = new String("he") + new String("llo");
String s2 = s1.intern();
System.out.println(s1 == s2);
JDK1.7版本:字符串常量池被移到了堆(heap)内存中
1. **new String("he")** 和 **new String("llo")** 分别创建了两个新的字符串对象。这些操作涉及的字符串字面量 **"he"** 和 **"llo"** 在编译时会被加入到字符串常量池中,但由于使用了 **new** 关键字,因此这里创建的是堆上的新对象。(此时有四个对象)
2. **new String("he") + new String("llo")** 通过 **StringBuilder** (实际执行过程中)拼接这两个字符串,生成了一个新的字符串对象 **s1**,其内容为 **"hello"**。这个步骤产生的字符串 **"hello"** 是一个新的对象,位于堆上,而不是常量池中。(此时有五个对象)
3. 当调用 **s1.intern()** 时,JDK 1.7 的行为是检查字符串常量池中是否存在内容等于 **s1** 的字符串:
- 如果存在,返回常量池中的那个字符串的引用。
- 如果不存在,将 **s1** 的引用添加到字符串常量池中。不是复制 s1,而是将堆上 s1 对象的引用添加到常量池。
4. 在这个具体案例中,由于 **"hello"** 是通过 **new String("he") + new String("llo")** 动态生成的,且在执行到 **s1.intern()** 之前,常量池中没有内容为 **"hello"** 的条目(注意,我们是通过字符串拼接创建的 **"hello"**,并非直接使用的字面量),所以 **s1.intern()** 会将 **s1** 的引用添加到字符串常量池中。(此时有五个对象,返回的是引用,没有复制新对象)
结论:返回true
---
JDK1.6版本:字符串常量池位于永久代
1. **new String("he")** 和 **new String("llo")**:这两个表达式分别创建了两个新的字符串对象在堆上,对应的字符串字面量 **"he"** 和 **"llo"** 在编译时被加入到字符串常量池中。(此时有四个对象)
2. **new String("he") + new String("llo")**:这个表达式通过 **StringBuilder** (或者类似的机制)拼接这两个字符串,生成了一个新的字符串对象 **s1**,内容为 **"hello"**。这个 **"hello"** 字符串对象是新创建的,存在于堆上,并且在这一步骤之前,字符串常量池中不会包含这个 **"hello"** 字符串的引用(假设之前没有其他操作将 **"hello"** 加入常量池)。(此时有五个对象)
3. **s1.intern()**:在 JDK 1.6 中,当你调用 **intern()** 方法时,如果字符串常量池中不存在一个等于此字符串对象的字符串时,它会将此字符串内容从堆上复制到常量池中(如果常量池中没有该内容),并返回常量池中的这个字符串引用。如果常量池中已经存在一个内容相同的字符串,则直接返回常量池中的那个字符串引用。
4. 在这个案例中,由于是首次出现 **"hello"** 字符串调用 **s1.intern()**,JDK 1.6 会将 **"hello"** 字符串从堆上复制到字符串常量池中,因为此时常量池中还不存在内容为 **"hello"** 的字符串。但请注意,这里是复制内容到常量池,常量池中的 **"hello"** 和堆上的 **s1** 是两个不同的对象。(此时有六个对象)
结论:返回false
### 字符串常量池
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。HotSpot 虚拟机中字符串常量池的实现是StringTable ,StringTable 可以简单理解为一个固定大小的HashTable ,容量为 StringTableSize(可以通过 -XX:StringTableSize 参数来设置),保存的是字符串(key)和 字符串对象的引用(value)的映射关系,字符串对象的引用指向堆中的字符串对象。
JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中。主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。
## 异常
### Exception 和 Error 有什么区别?
在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类:
1. Exception:程序本身可以处理的异常,可以通过 catch 来进行捕获,或者使用throws交由上层处理。Exception 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。受检异常是在编译时期就必须被处理的异常,如果没有被处理,那么程序在编译时期就无法通过编译。常见的有IOException等。不受检查异常包括RuntimeException 及其子类,程序不要求对这类异常进行处理,程序可以通过编译,如果这样的异常在方法中抛出且未被捕获,它会导致当前执行的线程终止,并且异常会被传递(propagate)到调用堆栈(call stack)中,直到遇到一个能够处理这个异常的catch块为止。如果最终这个异常没有在任何地方被捕获,那么它会达到应用程序的最上层,导致线程或者整个程序终止,并且可能会打印一个错误堆栈信息到标准错误输出。常见的有NullPointerException等。
2. Error:Error 属于程序无法处理的错误 ,不建议通过catch捕获 。例如 虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
Error可以被捕获吗?从技术上讲,**try-catch** 语句可以捕获任何 **Throwable** 的子类,包括 **Error** 和其子类。这意味着,尽管不推荐,但你确实可以用 **try-catch** 语句来捕获 **Error** 类型的异常。这些错误表示严重的系统问题,应用程序通常无法以有意义的方式处理它们。捕获这类错误可能会掩盖底层的严重问题,导致应用程序以未定义或不可预测的方式继续运行。在大多数情况下,最好是让这类错误不被捕获,以便能够通过错误日志或其他监控机制来发现并解决根本原因。
### Checked Exception 和 Unchecked Exception 有什么区别?
**Checked Exception(受检异常)**
受检异常是那些在编译时期就必须被处理(捕获或声明抛出)的异常。如果一个方法可能产生此类异常,但没有捕获它们(即在方法内用try-catch处理),那么必须在方法声明时使用throws关键字声明这些异常。这类异常必须要在运行前进行处理,否则程序在编译时期就无法通过编译。常见的有IOException,SQLException
**Unchecked Exception(不受检异常)**
不受检异常包括运行时异常(RuntimeException及其子类)。编译器不要求程序员显式地处理这些异常。这类异常反映了程序内部的逻辑错误,它们可以被捕获和处理,但编译器不强制要求这么做。常见的有NullPointerException,ArrayIndexOutOfBoundsException
常见的Checked Exception
- IOException:处理输入输出操作时,如读写文件操作,往往会遇到这种异常。
- SQLException:在进行数据库操作时,如查询、更新数据库时,可能会遇到此异常。
常见的Unchecker Exception
- NullPointerException(空指针错误)
- IllegalArgumentException(参数错误比如方法入参类型错误)
- NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)
- ArrayIndexOutOfBoundsException(数组越界错误)
- ClassCastException(类型转换错误)
- ArithmeticException(算术错误)
- SecurityException (安全错误比如权限不够)
- UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)
### Throwable 类常用方法有哪些?
- `String getMessage():` 返回异常发生时的简要描述
- `String toString():` 返回异常发生时的详细信息
- `String getLocalizedMessage():` 返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同
- `void printStackTrace(): `在控制台上打印 Throwable 对象封装的异常信息
### try-catch-finally 如何使用?
- `try`块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
- `catch`块:用于处理 try 捕获到的异常。
- `finally` 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。一般执行一些清理的工作以及资源的释放,比如关闭文件流或数据库链接等。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。
为什么finally的return会覆盖其他的return 这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。
### finally 中的代码一定会执行吗?
不一定(程序正常执行也不一定会被执行)
1. JVM退出:如果程序在 try 或 catch 块中调用了 System.exit() 方法来退出JVM,那么 finally 块不会被执行,因为JVM会直接终止当前运行的所有程序。
2. 线程被杀死:如果一个线程在执行 try 或 catch 块的代码时被强制终止或杀死,如调用线程的 stop() 方法(当然这个方法早在Java1.2版本就被废弃了),那么它的 finally 块可能不会执行。
3. 死锁:如果在 try 或 catch 块执行期间,线程进入到一个死锁状态,那么 finally 块也不会被执行。
4. 宿主机故障:如果宿主机遇到故障,比如断电,也会导致 finally 块不被执行。
System.exit()方法`System.exit()`方法是Java中用于终止当前运行的Java虚拟机(JVM)的方法。这个方法接受一个整型参数作为状态码,然后终止JVM。状态码通常用于指示程序的退出状态给调用者或操作系统;按照惯例,_非 0 的状态码(如1)表示异常或错误退出,而 0 通常表示正常退出_。
这个方法属于` java.lang.System` 类,因此它是静态的,可以直接通过类名调用。一旦 System.exit() 被调用,程序将停止执行,并且JVM会立即开始其终止过程。
为什么不推荐使用stop()方法当调用一个线程的 stop() 方法时,这个线程会立即停止执行,不管它当前在做什么。但实际上是非常危险的,因为它不保证资源的正常释放和数据的一致性。
1. 数据不一致:stop() 方法会导致线程立即停止,这可能发生在对共享资源或数据进行修改的中间。这种"粗暴"的停止方式可以留下未完成的数据状态,从而导致数据不一致或损坏。
2. 锁释放问题:在多线程环境中,线程可能会持有一些锁。如果一个线程被 stop() 方法强制停止,它持有的所有锁都会被立即释放,而不管资源的状态如何。这可能会导致其他线程访问到不一致的状态或未正确初始化的资源。
3. 留下清理工作未完成:由于线程被突然终止,任何试图确保资源被适当清理的 finally 块都不会得到执行。这可能导致打开的资源如文件或数据库连接等未被关闭。
因为上述原因,从JDK 1.2开始,stop() 方法就被标记为@Deprecated(过时)。
### 什么是 try-with-resources?
在Java7版本之前,资源的关闭通常在finally块中处理,但是这种方式实现起来代码冗长,并且需要在finally块中进行额外的异常处理等。
try-with-resources 是Java 7引入的一个特性,目的是简化在Java中进行资源管理的过程,如文件流、数据库连接等。使用这个结构时,所有实现了java.lang.AutoCloseable或java.io.Closeable接口的资源都可以被自动关闭。这两个接口都有一个close()方法,JVM会在try块执行完毕后自动调用它。这个语法结构确保了每个资源在语句结束时自动被关闭,从而避免资源泄露,同时也使得代码更加清晰和易于维护。
### 异常使用有哪些需要注意的地方?
- 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
- 抛出的异常信息一定要有意义。
- 建议抛出更加具体的异常比如字符串转换为数字格式错误的时候应该抛出NumberFormatException而不是其父类IllegalArgumentException。
- 避免重复记录日志:如果在捕获异常的地方已经记录了足够的信息(包括异常类型、错误信息和堆栈跟踪等),那么在业务代码中再次抛出这个异常时,就不应该再次记录相同的错误信息。重复记录日志会使得日志文件膨胀,并且可能会掩盖问题的实际原因,使得问题更难以追踪和解决。
## 泛型
### 为什么需要泛型?
泛型是JDK5引入的新的特性,提供了一种参数化类型的机制。
1. 可以提高代码的重用性,编写一次代码,使其适用于多种类型,而不需要为每种类型都编写一套相似的代码。
2. 编译器会自动检查泛型类型的兼容性,不再需要进行手动的类型转换。可以减少代码中的类型转换错误。
3. 泛型是在编译时确定类型的,可以避免Object类型引起的装箱和拆箱操作,从而提升效率。
### 泛型的上限和下限?
上限是使用关键字extends来指定的,比如<? extends Number>表示泛型类必须使用Number以及其子类才可以。
下限是使用关键字super来指定的,比如<? super String>表示泛型类必须使用String及其父类才可以。
### 伪泛型(类型擦除)
Java泛型是从JDK 1.5引入的,为了兼容之前的版本,Java泛型的实现采取了"伪泛型"的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的"类型擦除"(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。
### 泛型有哪几种使用方式?
1. 泛型类:泛型类是在类名后面添加一对尖括号 <>,并在里面放置一个类型参数来定义的。这个类型参数在类被实例化时指定。
2. 泛型接口:和泛型类定义方式相似。
3. 泛型方法:无论类是否被泛型化,都可以在其内部定义泛型方法。定义泛型方法时,必须在返回值前边加一个<T>,来声明这是一个泛型方法,持有一个泛型T,然后才可以用泛型T作为方法的返回值。
### 项目中哪里使用了泛型?
1. 在定义通用返回结果时,可以通过泛型根据具体的返回类型,动态指定结果的类型
2. 使用集合类中的排序等方法。
## 注解
### 什么是注解?
Annotation (注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。它可以用于很多方面,比如对编译器的指示,@Override注解告诉编译器重写方法,@NonNull告诉编译器当前变量不为null,或者用户编译时处理,如@Getter和@Setter都用于编译时期生成getter和setter方法。
### 什么是元注解?
元注解(Meta-annotations)是指那些应用于其他注解定义上的注解。
1. @Target:指定了注解可以应用的 Java 元素类型(如:类、字段、方法等)。
2. @Retention:指明注解在哪一个级别可用,即它们会保留到什么阶段。
3. @Documented:标记这样的注解应该被 javadoc 或类似的工具记录。
4. @Inherited:指示一个注解类型被自动继承。如果一个使用了 @Inherited 注解的注解被应用于某个类上,那么这个注解将默认地被该类的所有子类继承。
5. @Repeatable:在 Java 8 中引入,@Repeatable 允许同一个注解在同一个声明上被多次使用。
注解参数
1. @Target:它的参数是 ElementType 枚举的数组,表明该注解可以用在哪些程序元素上。
```java
@Target(ElementType.METHOD) // 只能应用于方法上
public @interface MyMethodAnnotation {}
- ElementType.TYPE:类、接口(包括注解类型)或枚举声明
- ElementType.FIELD:字段声明(包括枚举常量)
- ElementType.METHOD:方法声明
- ElementType.PARAMETER:参数声明
- ElementType.CONSTRUCTOR:构造方法声明
- ElementType.LOCAL_VARIABLE:局部变量声明
- ElementType.ANNOTATION_TYPE:注解类型声明
- ElementType.PACKAGE:包声明
- ElementType.TYPE_PARAMETER:类型参数声明(Java 8 新增)
- ElementType.TYPE_USE:类型使用声明(Java 8 新增)
- @Retention:它的参数是 RetentionPolicy 枚举,表示注解的保留策略。
java
@Retention(RetentionPolicy.RUNTIME) // 运行时可用
public @interface MyRuntimeAnnotation {}
- RetentionPolicy.SOURCE:注解只在源代码中保留,编译器将丢弃
- RetentionPolicy.CLASS:注解在编译器将类文件输出时保留,但在运行时不保留(这是默认行为)
- RetentionPolicy.RUNTIME:注解在运行时保留,可通过反射访问
- @Documented:它没有参数。
java
@Documented
public @interface MyDocumentedAnnotation {}
- @Inherited:它没有参数。
java
@Inherited
public @interface MyInheritedAnnotation {}
- @Repeatable:一个注解数组
java
@Repeatable(MyAnnotations.class)
public @interface MyRepeatableAnnotation {
String value();
}
public @interface MyAnnotations {
MyRepeatableAnnotation[] value();
}
在这个例子中,@Repeatable注解的参数是一个,包含 由 当前注解组成的数组 作为参数 的注解的class。可以参考下面的例子理解。
java
@interface Role {
String value();
}
@interface Roles {
Role[] value();
}
@Roles({@Role("Admin"), @Role("User")})
public class User {}
java
@Repeatable(Roles.class)
@interface Role {
String value();
}
@interface Roles {
Role[] value();
}
@Role("Admin")
@Role("User")
public class User {}
注解的解析方法有哪几种?
- 通过反射API直接解析:在运行时,可以通过 Java 的反射 API 如 Class.getAnnotation(), Method.getAnnotations(), Field.getAnnotation() 等方法来检查类、方法、字段等元素上的注解信息。通过这些方法可以获取到元素上指定类型的注解,然后根据注解类型的定义来获取其中的信息。
- 在编译时,可以使用注解处理器来处理源代码中的注解。
- 一些框架或库提供了自己的机制来解析和处理注解,简化了注解的使用和处理。
Class.getAnnotation(), Method.getAnnotations(), Field.getAnnotation()### 1. Class.getAnnotation(Class annotationClass)
此方法用于获取应用到特定类上的指定类型的注解。
- 参数 :annotationClass ------ 指定想要获取的注解的 Class 对象。
- 返回 :如果指定类型的注解存在于此元素上,则返回该注解;否则返回 null。
- 用法示例:
java
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation {}
@MyAnnotation
class MyClass {}
// 稍后
MyAnnotation myAnnotation = MyClass.class.getAnnotation(MyAnnotation.class);
if (myAnnotation != null) {
// 类上存在 MyAnnotation 注解
}
2. Method.getAnnotations()
此方法用于获取应用到特定方法上的所有注解。
- 返回:一个注解数组,包含此方法上的所有注解。如果该方法没有注解,则返回一个空数组。
- 用法示例:
java
class MyClass {
@MyAnnotation
public void myMethod() {}
}
// 稍后
Method method = MyClass.class.getMethod("myMethod");
Annotation[] annotations = method.getAnnotations();
for (Annotation annotation : annotations) {
// 处理 method 上的每个注解
}
3. Field.getAnnotation(Class annotationClass)
此方法用于获取应用到特定字段上的指定类型的注解。
- 参数 :annotationClass ------ 指定想要获取的注解的 Class 对象。
- 返回 :如果指定类型的注解存在于此字段上,则返回该注解;否则返回 null。
- 用法示例:
java
class MyClass {
@MyAnnotation
private int myField;
}
// 稍后
Field field = MyClass.class.getDeclaredField("myField");
MyAnnotation myAnnotation = field.getAnnotation(MyAnnotation.class);
if (myAnnotation != null) {
// 字段上存在 MyAnnotation 注解
}
这些方法使得我们能够在运行时通过反射来检查 Java 类、方法或字段上的注解信息,进而根据注解提供的元数据来改变程序的行为或执行特定的逻辑。这在诸如依赖注入框架、ORM 映射、Web 框架中尤其有用。
反射
什么是反射?
反射(Reflection)是 Java 提供的一种机制,它允许运行中的 Java 程序对自身进行检查,并且能够直接操作程序内部属性、方法、构造器等成员。通俗的讲,通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
反射的应用场景?
正是因为反射,才能轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。
- 动态代理:在这些框架中大量使用了动态代理模式。它在运行时创建一个代理对象。这个代理对象可以用于方法调用的拦截、处理、转发等目的。
- Spring 框架的依赖注入:Spring 使用 Java 反射 API 来实现依赖注入。反射允许 Spring 在运行时查询类的信息(如构造函数、方法、字段等),并动态地调用它们。
- 数据库的ORM映射:MyBatis 使用 Java 反射来动态生成Mapper的实现,在运行时动态创建 DAO(数据访问对象)实现,以及将 SQL 查询结果转换成 Java 对象。
java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class DynamicProxyDemo {
public static void main(String[] args) {
// 被代理的实例
HelloService originalService = new HelloServiceImpl();
// 动态代理对象参数1:类加载器,它将用于加载代理类
ClassLoader classLoader = HelloService.class.getClassLoader();
// 动态代理对象参数2:接口数组,代理类需要实现的接口列表
Class<?>[] interfaces = new Class<?>[]{HelloService.class};
// 动态代理对象参数3:调用处理器,当代理实例的方法被调用时,会转发到这里的 invoke 方法。
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 只对 sayHello 方法加入日志
if ("sayHello".equals(method.getName())) {
System.out.println("Before method call: " + method.getName());
Object result = method.invoke(originalService, args); // 调用原始对象的方法
System.out.println("After method call: " + method.getName());
return result;
}
// 对于其他方法,直接调用原始对象的方法,不加入日志
return method.invoke(originalService, args);
}
};
// 创建动态代理
HelloService proxyService = (HelloService) Proxy.newProxyInstance(
classLoader,
interfaces,
handler
);
// 通过代理对象调用方法
proxyService.sayHello("World"); // 这将加入日志
proxyService.sayGoodBye("World"); // 这不会加入日志
}
}
反射的优缺点?
优点:可以使代码更加灵活、为各种框架提供开箱即用的功能,提供了便利,可以在不修改源代码的情况下,通过外部配置来实现新的功能或更改原有的功能。
缺点:反射操作通常比直接调用方法或访问属性要慢,并且反射机制可以绕过编译时的类型检查,存在一定的安全隐患。反射机制可以访问和修改对象的私有属性和方法,这可能会导致安全漏洞。
获取Class对象方式?
- 知道具体类的情况下:
java
Class alunbarClass = TargetObject.class;
- 通过 Class.forName()传入类的全路径获取:
java
Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject");
- 通过对象实例instance.getClass()获取:
java
TargetObject o = new TargetObject();
Class alunbarClass2 = o.getClass();
- 通过类加载器xxxClassLoader.loadClass()传入类路径获取:
java
ClassLoader.getSystemClassLoader().loadClass("cn.javaguide.TargetObject");
Class类常用的方法
- getName(): 返回类的全限定名。
- getSimpleName(): 返回类的简单名称(不包含包名)。
- getCanonicalName(): 返回类的规范化名称。
- getClassLoader(): 返回加载该类的类加载器。
- getSuperclass() : 返回该类的父类的 Class 对象。
- getInterfaces() : 返回表示该类实现的接口的 Class 对象数组。
- newInstance(): 创建该类的一个新实例(已过时,不推荐使用)。
- isAssignableFrom(Class<?> cls): 判断该类是否可以从指定类派生。
- isInstance(Object obj): 判断指定对象是否为该类的实例。
- getField(String name) : 返回一个 Field 对象,它反映此 Class 对象所表示的类或接口的指定公共成员字段。
- getDeclaredField(String name) : 返回一个 Field 对象,它反映此 Class 对象所表示的类或接口的指定已声明字段。
- getMethods() : 返回一个包含 Method 对象的数组,这些对象反映此 Class 对象表示的类或接口的所有公共方法,包括从超类继承的方法。
- getDeclaredMethods() : 返回一个包含 Method 对象的数组,这些对象反映此 Class 对象表示的类或接口声明的所有方法,包括私有方法。但不包括从超类继承的方法。
- newInstance(): 创建该类的一个新实例(已过时,不推荐使用)。
SPI(待补全)
什么是SPI?
SPI 即 Service Provider Interface ,字面意思就是:"服务提供者的接口",是专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。举例来说,调用方有一套接口规则,不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。
SPI的使用场景?
- JDBC:在java中定义了接口java.sql.Driver,并没有具体的实现,具体的实现都是由不同厂商来提供的。