Java SE 的一些问题笔记
Java SE 的细节问题
什么是面向对象?谈谈你对面向对象的理解
面向对象思想:
将事物抽象成一个个类,可以帮助我们更加高效的去解决问题,代码将可重用,好维护,扩展性强。
面向对象的三大特征:
-
封装
类中不想向外部直接暴露的属性将其使用 private 修饰,如果需要操作被 private 修饰的属性,可对外提供对应的 setter/getter 方法即可。隐藏了对象的属性和实现细节,仅仅对外提供接口。 -
继承
当子类继承父类时,就可以拥有父类的属性和方法,无需重新编写父类的属性和方法直接拥有,并且在其基础上进行功能扩展以及有自己的属性和方法。需要注意的是父类被 private 修饰的属性和方法是不会被子类继承的。
-
多态
同一个行为有不同的表现方式。
多态的特点:继承,重写,父类引用指向子类对象。
面向对象编程思想中,最重要的就是继承和多态这两个概念,继承是面向对象的一个基石,提高了代码复用性,多态的前提是类之间必须要有继承,多态提高了代码的可扩展性。
Java 泛型了解么?什么是类型擦除?泛型的三种使用方式?
Java 泛型是 JDK5 中应用的一个新特性,它的作用是编译时提供了类型安全检查,这个机制让程序员在编译时就检查到非法的类型。泛型的本质是参数化类型,也就是说将所操作的数据类型指定为参数。
Java 的泛型是伪泛型,因为 Java 在运行期间,所有的泛型信息都会被擦除,也就是通常说的类型擦除。
参数化类型:也就是说所操作的数据类型被指定为一个参数。
泛型的使用方式:
-
泛型类
public class Context<T>{ private T key; public void Context(T key){ this.key = key; } public T getKey(){ return key; } }
实例化类:
Context<String> context = new Context<String>("Tom");
一般作用是在于容器类上。
-
泛型接口
-
泛型方法
泛型的作用
- 类型安全
- 消除强制类型转换
- 更好的代码复用性
==与 equals 的区别
==作用于基本数据类型比较的的是值,引用数据类型比较的是对象的内存地址。
equals()
不能用于基本数据类型值(并不包括基本数据类型的包装类)的变量,只能比较两个对象是否相等,equals() 方法存在于Object 类中:
public boolean equals(Object obj) {
return (this == obj);
}
注意:一个类中如果没有重写 equals() 方法,默认使用的是Object类中的 equals() 的方法,这个时候比较的就是对象的内存地址了。
String 和 new String() 的区别
String
String str = "abc";
这一代码会在字符串常量池中查找“ abc ”,如果没有则新建一个字符串对象,然后将 str 指向“ abc ”,String str2 = "abc";
此时 srt2 会直接在字符串常量池中找到“abc”对象并引用
new String()
String str3 = new String("abc") 很明显就是在堆区创建对象并分配内存,虽然其中的String 实例的 value 指向的是常量池中的“abc”,但是最终还是堆区对象,如果 str3 == str2 返回的是false,因为 str2 引用的是常量池中的“abc”,而 str3 引用的是堆区新建的 String 实例,所以比较的地址肯定是不相等的。那为什么两者的 value 地址相等?注意:== 真正比较的是两者的内存地址,而不是 value,value 只是 String 中的属性,str3 引用的 String 实例中的 value 属性就是运用了常量池中的 “abc” 的 value。
hashCode()与equals()
final 关键字的作用
final 关键字作用在不同的地方效果不同:
- 修饰类,则该类不能被继承,如果需要可以手动设置成员变量为 final,但是该类下的所有成员方法都会被隐式的指定为 final 方法。
- 修饰方法,被修饰的方法不会被覆盖(重写),另外类中所有被 private 修饰的都会隐式添加 final,因为 private 修饰的方法外部不可见,无法被重写,所以会隐式添加 final
- 修饰方法形参,如果是基本数据类型,则表示不能修改值,如果是引用数据类型则可以修改其对象的值,实际上就算不加 final,形参的修改也无法影响实参。
- 修饰变量,就是声明常量。
为什么 Java 只有值传递。
首先我们要搞清楚程序语言中,传递过程中的传递分为两种情况:即值传递和引用传递。
值传递:在方法调用的时候,传递的参数是实际参数的副本。这样在函数中如果对参数进行修改,实际参数是不受影响的。
引用传递:在方法调用的时候,传递的参数是实际参数的内存地址,这样在函数中对参数进行修改,将会影响到实际参数。
我们的重点应该是关注传递的参数是否是实际参数的副本。
当实际参数是对象时,容易混淆两者的关系。当我们实参是对象时,其实传递的是实参地址的副本。
根据引用传递的说法,我们传递的确实是实参的内存地址,但是我们需要注意的一点是,我们在函数中对参数进行修改,是否正真的影响了实际参数。在判断实参是否受影响的时候,我们应该注意实参传入的是什么?如果实参传入的是地址,则需要关注地址是否发生了影响,而不是地址指向的对象的变化。
这样一来,Java 中是值传递了,因为 Java 在基本数据类型中传递的是值的副本,在引用数据类型中传递的是地址的副本。在函数中对形参进行操作,如果操作的是地址指向对象的内容,那肯定是不影响地址本身,而是对象内容,如果操作的是将形参地址指向一个新的地址,此时实参的地址也不会发生变化。这就是为什么 Java 只有值传递的原因。
StringBuffer & StringBuilder 有什么区别?
我们通过查看源码发现两者都继承了 AbstractStringBuilder
这个类。
可以看到有一个 char[]
属性,并没有像 String 一样是被 final
修饰,所以这就决定了两者是可变的(两者构造方法都是调用了父类的构造方法),而 String 是不可变的。
线程安全
String 中的对象是不可变的,可以理解为常量,所以是线程安全。StringBuffer 方法加了同步锁或对调用的方法加了同步锁,所以线程是安全的。StringBuilder 方法没有加同步锁,所以线程不安全。
性能
String 类型进行改变时,都会新建一个 String 对象,然后指向新的 String 对象。StringBuffer 是对本身进行操作,并不会新建对象。由于 StringBuilder 不是线程安全的所以通常情况下效率是要高于 StringBuffer 的。
使用
少量字符串操作直接使用 String
单线程操作字符串缓冲区下操作大量数据: StringBuilder
多线程操作字符串缓冲区下操作大量数据:StringBuffer
为什么说String不可变?
通过看 String 的源码得知,实际上 String 的字符串内容时一个字符数组 : private final char value[];
并且这个字符数组被 final
修饰了,这就代表了 String 是不可变的。
我们来看看什么是不可变对象:
(1)类内部所有的字段都是final修饰的。
(2)类内部所有的字段都是私有的,也就是被private修饰。
(3)类不能够被集成和拓展。、
(4)类不能够对外提供哪些能够修改内部状态的方法,setter方法也不行。
(5)类内部的字段如果是引用,也就是说可以指向可变对象,那我们程序员不能获取这个引用。
而我们的 String 类型正好满足。
为什么要设计 String 不可变?
java变量自增问题(i++和++i的区别)
实际上 i++;
和 ++i;
的执行过程和结果是一样的,主要是区分这两个的赋值过程。
code:
@Test
public void demo_01(){
int i = 0;
int a = i++; // 0
System.out.println(a);
i = 0;
int b = ++i; // 1
System.out.println(b);
}
看到 a = i++;
这行代码,结果是 i=0,因为 i++ 是先赋值再计算。
看到 b = ++i;
这行代码,结果是 i=1,因为 ++1 是先计算再赋值。
因为在两者赋值的时候运算和引用的顺序不一样(字节码文件详细),所以只要记住口诀就可以了。还有一点就是 ++i 可以做左边值,而 i++ 只能做右边值。
ArrayList 和 LinkedList 有什么区别?
考点:主要考察面试者对两者的数据结构的理解。
两者都是线性结构,主要的区别是结构不同。
ArrayList
- 用的是桶位数组为数据存储核心。
- 由于有索引,查询非常快。
- 增删改稍慢。
LinkedList
- 用的是对象的引用为核心。
- 对比ArrayList占用内存,一个节点包含了前后两个节点的引用。
- 查询慢,增删改快。
Java 中接口和抽象类的区别
相同点:
- 都不能被实例化
- 都能被作为引用类型
- 一个类继承抽象类或实现接口,必须实现抽象方法和接口定义的方法,否则这个类必须是抽象类或接口。
不同点:
接口:
- 只能有方法,成员变量实际上是常量,因为默认public static final修饰的静态常量。
- 不能有静态方法
- 成员只能是public
- 接口可以多实现
抽象类:
- 可以有自己的构造方法
- 成员可以是private、默认、protected、public
- 有抽象方法的一定是抽象类,但是抽象类不一定有抽象方法。
- 可以包含静态方法。
- 只能单继承
什么时候使用接口?什么时候使用抽象类?
抽象类:
将一个概念将其抽象,对类抽象,例如:动物,植物,食物。。。我们需要去描述一类事物的基本共同属性时,使用抽象类将其描述。
接口:
用于规范事物的特征或行为(方法),局部抽象,例如:飞翔,潜水。。。只要有这些行为的类都能实现对应接口,实现具体方法。
抽象类是对类抽象而接口则是对类的行为的约束和统一规范。
重载和重写的区别?
重载发送在同一个类中,方法名必须相同,参数个数,排列,以及类型不同,来区分重载的方法,访问修饰符和返回值可以不同,发送在编译期间。
重写发生在子类与父类之间,方法名和参数列表相同,返回值和抛出异常范围小于父类的范围,访问修饰符范围必须大于或等于父类的范围。
注意重载方法之间的特点是:方法名相同,参数列表不同,返回值不能作为用来区分重载方法。
Java中的四个引用
JDK 1.2之前,对象的引用只分为两种 “已被引用”和“未被引用”。无法描述特殊情况下的对象,比如,内存紧张时,应该抛弃一些对象来释放内存。
所以在 JDK 1.2之后,对其对象的状态进行了扩展,分为“强引用”,“软引用”,“弱引用”,“虚引用”
强引用:
A a = new Object();
只要 a 还指向 Object 对象,则Object 对象不会被垃圾回收,除非 A a = null;
手动置为 null。
只要强引用存在,对象就永远不会被垃圾回收机制回收,就算内存不足,JVM也只会抛出内存溢出异常。手动置为 null,垃圾回收器会在合适的时候回收对象。
软引用:
软引用用来描述一些非必须但仍有用的对象,内存足够则不会被回收,当内存不足时,垃圾回收器会回收这些对象,如果回收之后,还是不足,则会提示内存溢出异常。常常用于缓存技术,网页缓存,图片缓存等。。。
弱引用:
比软引用弱一点,无论内存是否足够,只要 JVM 垃圾回收机制启动,则会回收弱引用相关联的对象。
虚引用:
持有虚引用的对象,任何时候都有可能被回收,因为就和没有一样。