文章目录
- 2.1.1 重载和重写的区别
- 2.1.2 String 和 StringBuffer、StringBuilder 的区别是什么?String 为什么是不可变的?
- 2.1.3 自动装箱与拆箱
- 2.1.4 == 与 equals
- 2.1.5 关于 final 关键字的一些总结
- 2.1.6 Object类的常见方法总结
- 2.1.7 Java 中的异常处理
- 2.1.8 获取用键盘输入常用的的两种方法
- 2.1.9 接口和抽象类的区别是什么
- 2.1.10 字符型常量和字符串常量的区别?
- 2.1.11 构造器 Constructor 是否可被 override?
- 2.1.12 在一个静态方法内调用一个非静态成员为什么是非法的?
- 2.1.13 在 Java 中定义一个不做事且没有参数的构造方法的作用
- 2.1.14 hashCode 与 equals(为什么重写 equals 时必须重写 hashCode ?)
- 2.1.15 Java 序列化中如果有些字段不想进行序列化,怎么办?
- 2.1.16 浅拷贝 与 深拷贝
本文主要源自 JavaGuide 地址:https://github.com/Snailclimb/JavaGuide 作者:SnailClimb
仅供个人复习使用
2.1.1 重载和重写的区别
区别点 | 重载 | 重写 |
---|---|---|
产生地点 | 同一个类中 | 父子类中 |
参数列表 | 必须修改 | 一定不能修改 |
返回类型 | 可以修改 | 一定不能修改 |
抛出异常的范围 | 可以修改 | 子类小于等于父类 |
访问修饰符范围 | 可以修改 | 子类大于等于父类,如果父类方法访问修饰符为 private 则子类就不能重写该方法 |
发生阶段 | 编译期 | 运行期 |
2.1.2 String 和 StringBuffer、StringBuilder 的区别是什么?String 为什么是不可变的?
① 可变性
String类中使用final关键字修饰字符数组来保存字符串,private final char[] value
,因此String对象是不可变的;
而 StringBuffer 和 StringBuilder 都继承自AbstractStringBuilder类,在AbstractStringBuilder类也是使用字符数组char[] value
保存字符串,但是没有用final修饰,因此 StringBuffer 和 StringBuilder 对象是可变的。
StringBuffer 和 StringBuilder 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的。
AbstractStringBuilder.java:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count;
AbstractStringBuilder() {
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
② 线程安全性
String对象是不可变的,可以理解为常量,因此是线程安全的。
AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用父类的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
③ 性能
每次对 String 对象进行改变时,都会在字符串常量池中生成一个新的 String 对象,然后将栈内存地址指向新的String对象。
StringBuilder 与 StringBuffer每次都会对对象自身进行操作,而不是生成新的对象并改变对象引用。由于StringBuilder 并没有对方法进行加同步锁,所以性能上StringBuilder会更有速度优势,但却有多线程不安全的风险。
三者的区别总结:
- String:不可变,效率很低,线程安全;
- StringBuffer:可变,效率低,线程安全;
- StringBuilder:可变,效率高,线程不安全;
三者的使用总结:
- 操作少量数据:String
- 单线程操作大量数据:StringBuilder
- 多线程操作大量数据:StringBuffer
2.1.3 自动装箱与拆箱
java se5之前,如果要生成数值为10的Integer对象,则需要写成
Integer i = new Integer(10);
从java se5开始,就提供了自动装箱的特性,可以写成
Integer i = 10; //装箱
int n = i; //拆箱
简单讲,装箱是自动将基本数据类型转换为包装器类型,拆箱是自动将包装器类型转换为基本数据类型。
一句话总结装箱和拆箱的实现过程:
装箱过程是通过调用包装器的valueOf方法实现的,如Integer的valueOf(int)方法,而拆箱过程是通过调用包装器的 xxxValue方法实现的(xxx代表对应的基本数据类型),如Integer的intValue()方法。
面试相关问题:
- 下面这段代码:
public class Main {
public static void main(String[] args) {
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i1==i2);
System.out.println(i3==i4);
}
}
输出结果是
true
false
输出结果表明i1和i2指向的是同一个对象,而i3和i4指向的是不同的对象。此时需要查看源码,下面是Integer的valueOf(int)方法的具体实现:
public static Integer valueOf(int i) {
if(i >= -128 && i <= IntegerCache.high)
return IntegerCache.cache[i + 128];
else
return new Integer(i);
}
可以看出,在通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。
上面的代码中i1和i2的数值为100,因此装箱时i1和i2指向的是常量池中的同一个对象,而i3和i4装箱时则是分别指向不同的对象。
- 下面这段代码:
public class Main {
public static void main(String[] args) {
Double i1 = 100.0;
Double i2 = 100.0;
Double i3 = 200.0;
Double i4 = 200.0;
System.out.println(i1==i2);
System.out.println(i3==i4);
}
}
输出结果是
false
false
为什么会和Integer不同?因为在某个范围内的整型数值的个数是有限的,可以存在常量池中,需要时直接指向,而浮点数却不是。
-
谈谈
Integer i = new Integer(xxx)
和Integer i =xxx
这两种方式的区别。
1)第一种方式不会触发自动装箱的过程,第二种方式会触发
2)第二种方式的执行效率和资源占用一般会优于第一种方式。因为第一种方式每次都会在堆内存中new一个新的对象,而第二种方式有可能会指向已经存在于常量池中的对象。 -
下面这段代码的输出结果:
public class Main {
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
Long h = 2L;
System.out.println(c==d); //true
System.out.println(e==f); //false
System.out.println(c==(a+b)); //true
System.out.println(c.equals(a+b)); //true
System.out.println(g==(a+b)); //true
System.out.println(g.equals(a+b)); //false
System.out.println(g.equals(a+h)); //true
}
}
需要注意,
当 "=="运算符的两个操作数都是 包装器类型的引用,则是比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程)。 如第3句,a+b属于算术运算,会触发自动拆箱过程(即调用intValue方法),因此它们比较的是数值是否相等。第5句中,a+b运算后,会自动类型提升,调用Integer的longValue方法。
另外,对于包装器类型,equals方法并不会进行类型转换。 如第6句,a+b会先各自调用intValue方法,得到了加法运算后的数值之后,便调用Integer.valueOf方法,再进行equals比较,由于g是Long类型的对象,a+b是Integer类型的,且equals方法不会进行类型转换,所以返回false。
2.1.4 == 与 equals
对于基本数据类型,equals和==都是比较数据的值,无区别。
对于复合数据类型,也就是类,==比较的是两个对象在堆内存中的地址,而equals分为两种情况:
(1)类没有覆写equals方法。则默认使用的是Object中继承的equals方法,此时equals和==一样,都是比较两个对象在堆内存中的地址;
(2)类重新覆写了equals方法。一般,我们都通过覆写equals方法来比较两个对象的内容是否相等,如果内容相等,则返回true。
补充:栈内存,堆内存,常量池
栈内存: 存放局部基本类型变量,对象的引用和方法调用;
堆内存: 存放所有new出来的对象和数组;
运行时常量池: 存放字符串常量和基本类型常量。JDK1.7 及之后的版本,JVM 已经将字符串常量池单独从方法区中移了出来,在 Java 堆中开辟了一块区域存放字符串常量池。
String的创建方式有两种:
- 直接赋值
此方式在方法区中字符串常量池中创建对象
String str = “flyapi”; - 构造器
此方式在堆内存创建对象
String str = new String();
1.举例
String str1 = "HelloFlyapi";
String str2 = "HelloFlyapi";
System.out.println(str1 == str2); // true
当执行第一句时,JVM会先去字符串常量池中查找是否存在HelloFlyapi,当存在时直接返回字符串常量池里的引用;当不存在时,会在字符串常量池中创建一个对象并返回引用。
当执行第二句时,同样的道理,由于第一句已经在字符串常量池中创建了,所以直接返回上句创建的对象的引用。
2.举例
String str1 = "HelloFlyapi";
String str3 = new String("HelloFlyapi");
System.out.println(str1 == str3); // false
执行第一句,同上第一句。
执行第二句时,会在堆(heap)中创建一个对象,当字符串常量池中没有‘HelloFlyapi’时,会在字符串常量池中也创建一个对象;当字符串常量池中已经存在了,就不会创建新的了。然后将堆中的对象指向字符串常量池中的对象。
3.举例
String str1 = "HelloFlyapi";
String str6 = "Hello" + "Flyapi";
System.out.println(str1 == str6); // true
由于”Hello”和”Flyapi”都是常量,编译时,第二句会被自动编译为‘String str6 = “HelloFlyapi”;
对于equals和==,举个例子:
public class test1 {
public static void main(String[] args) {
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
}
}
说明:
- String 中的 equals 方法是被重写过的,比较的是对象的内容。
- 当创建 String 类型的对象时(new 出来的),JVM首先会在字符串常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有,就让堆中的 String 对象直接指向字符串常量池中的对象。如果没有就在字符串常量池中重新创建一个 String 对象,再令堆中的对象指向字符串常量池中的对象。
2.1.5 关于 final 关键字的一些总结
final关键字主要用在三个地方:变量、方法、类。
- 对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象(即保证它的地址不变,但这个对象的内容可以发生改变)。
// final修饰Person变量,p是一个引用变量
final Person p = new Person(45);
// 改变Person对象的age实例变量,合法
p.setAge(23);
-
当用final修饰一个类时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。
-
使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。
tips:什么是引用类型? 除掉八种基本数据类型,其它的都是对象,也就是引用类型,包括数组。
2.1.6 Object类的常见方法总结
Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:
//native方法,用于获取对象的运行时对象的类。,使用了final关键字修饰,故不允许子类重写。
public final native Class<?> getClass()
//native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。
public native int hashCode()
//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。
public boolean equals(Object obj)
//naitive方法,用于创建并返回当前对象的一份拷贝。
//一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。
//Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。
protected native Object clone() throws CloneNotSupportedException
//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。
public String toString()
//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
public final native void notify()
//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
public final native void notifyAll()
//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。
public final native void wait(long timeout) throws InterruptedException
//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。
public final void wait(long taimeout, int nanos) throws InterruptedException
//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
public final void wait() throws InterruptedException
//实例被垃圾回收器回收的时候触发的操作
protected void finalize() throws Throwable { }
tips:什么是native方法? 简单讲,一个Native Method就是一个java调用非java代码的接口
2.1.7 Java 中的异常处理
java异常类层次结构图
如图可以看出所有的异常跟错误都继承与Throwable类,也就是说所有的异常都是一个对象。
从大体来分异常为两块:
1、error—错误 : 是指程序无法处理的错误,表示应用程序运行时出现的重大错误。例如jvm运行时出现的OutOfMemoryError以及Socket编程时出现的端口占用等程序无法处理的错误。
2、Exception — 异常 :异常可分为运行时异常跟编译异常
- 1)运行时异常:即RuntimeException及其子类的异常。这类异常在代码编写的时候不会被编译器所检测出来,是可以不需要被捕获,但是程序员也可以根据需要进行捕获抛出。常见的RUNtimeException有:NullpointException(空指针异常),ClassCastException(类型转换异常),IndexOutOfBoundsException(数组越界异常)等。
- 2)编译异常:RuntimeException以外的异常。这类异常在编译时编译器会提示需要捕获,如果不进行捕获则编译错误。常见编译异常有:IOException(流传输异常),SQLException(数据库操作异常)等。
注意:异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。
看代码:
try{
//待捕获代码
}catch(Exception e){
System.out.println("catch is begin");
return 1 ;
}finally{
System.out.println("finally is begin");
}
最后代码的输出为:
catch is begin
finally is begin
说明:代码会先执行catch里面的代码,然后执行finally里面的代码,最后才return1 ;
看代码:
try{
//待捕获代码
}catch(Exception e){
System.out.println("catch is begin");
return 1 ;
}finally{
System.out.println("finally is begin");
return 2 ;
}
最后代码会return 2。说明:执行了finally后已经return了,所以catch里面的return不会被执行到。也就是说finally永远都会在catch的return前被执行。(这个是面试经常问到的问题哦!)
异常处理总结
- try 块:用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。
- catch 块:用于处理try捕获到的异常。
- finally 块:无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return语句
时,finally语句块将在方法返回之前被执行。
在以下4种特殊情况下,finally块不会被执行:
- 在finally语句块中发生了异常。
- 在前面的代码中用了System.exit()退出程序。
- 程序所在的线程死亡。
- 关闭CPU。
2.1.8 获取用键盘输入常用的的两种方法
方法1:Scanner类
Scanner input = new Scanner(System.in);
String str = input.nextLine();
input.close();
方法2:BufferedReader类
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String str = input.readLine();
2.1.9 接口和抽象类的区别是什么
- 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),抽象类可以
有非抽象的方法 - 接口中的实例变量默认是 final 类型的,而抽象类中则不一定
- 一个类可以实现多个接口,但最多只能实现一个抽象类
- 一个类实现接口的话要实现接口的所有方法,而抽象类不一定
- 接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口的对象 从设计层面来说,抽象是对类的抽
象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
关于第5点的说明:
//定义接口Handler
public interface Handler{
public void Hello();
}
//实现接口的类MyHandler
public class MyHandler implements Handler{
public void Hellp(){
System.out.println( "my Handler implements " );
}
}
//在对接口的引用时,采用的是实例化实现该接口的类
Handler handler = new MyHander();
接口可以被声明出来(如Handler handler;
),但决不能实例化,它可以作为子类的引用指向子类的实例,但是不能通过handler来调用子类所特有方法。
备注:在JDK8中,接口也可以定义静态方法,可以直接用接口名调用。如果同时实现两个接口,接口中定义了一样的默认方法,必须重写,不然会报错。
2.1.10 字符型常量和字符串常量的区别?
- 形式上:字符型常量是用单引号引起的单个字符,字符串常量是用双引号引起的若干个字符
- 含义上:字符型常量相当于一个整型变量(ASCII码),可以参与数值运算,而字符串常量代表一个地址值(该字符串在内存中的存放位置)
- 占内存大小::字符变量只占2个字节(注意:java中的char占2个字节),字符串变量占若干个字节(和地址长度有关)
2.1.11 构造器 Constructor 是否可被 override?
Constructor不能被重写(override),但是可以被重载(overload)。因此,一个类中可以有多个构造函数。
2.1.12 在一个静态方法内调用一个非静态成员为什么是非法的?
由于静态方法不可以通过对象来调用,所以在静态方法中,不能调用其他非静态变量,也不可以访问非静态变量。
2.1.13 在 Java 中定义一个不做事且没有参数的构造方法的作用
Java程序在执行子类的构造方法之前,如果没有用super()
调用父类中指定的构造方法,则默认调用父类中没有参数的构造方法。
因此,如果父类中只定义了有参数的构造方法,而子类的构造方法中又没有用super()
调用该方法,则会编译错误,因为Java程序在父类中找不到没有参数的构造函数执行。
解决方法就是在父类中定义一个不做事且没有参数的构造方法。
2.1.14 hashCode 与 equals(为什么重写 equals 时必须重写 hashCode ?)
hashCode介绍
hashCode()是获取对象的hash码,也称为散列码。它会返回int整数,这个hash码的作用是确定该对象在散列表中的索引位置。
为什么要有hashCode
以HashSet为例:当你把对象加入HashSet时,HashSet会首先用hashCode()
计算对象的hash码。然后用hash()
方法经过扰动后,看该hash码对应位置上是否已有其他对象,如果有,则会用equals()
与该位置的其他对象逐一比对。假如发现重复,则不允许添加,并返回false。
hashCode和equals的相关规定
- 如果两个对象相等,则它们的hashCode一定是相同的
- 如果两个对象相等,则它们调用equals方法后返回true
- 如果两个对象的hashCode相同,它们也不一定是相等的
- hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode方法,则相同class的两个对象无论如何都不会相等(即使是同一个对象)。
- 因此,如果equals方法被重写过,则hashCode方法也一定要重写
2.1.15 Java 序列化中如果有些字段不想进行序列化,怎么办?
对于不想序列化的变量,使用transient
进行修饰。
transient
的作用是:防止那些用transient
修饰的变量被序列化。
当对象被反序列化时,被transient
修饰的变量不会被持久化和恢复(持久化就是将内存中的数据保存起来,使之可以长期存在)。transient
只能修饰变量,不能修饰类和方法。
2.1.16 浅拷贝 与 深拷贝
浅拷贝:对基本数据类型进行值传递;对引用类型,也是进行值传递,只不过传递的是对象的地址,所以两个引用类型会指向同一个对象。
深拷贝:对基本数据类型进行值传递;对引用类型,创建一个新的对象,并复制其内容到新对象,所以两个引用类型会指向不同对象。