Java面试资料
“迷失的时候,选择更艰辛的那条路。”
———松浦弥太郎
一、Java基础
1.基本数据类型:
整数型(整数):byte,short,int,long
浮点型(小数):float,double
字符型:char
布尔型:boolean(true,false)
数据类型 | 型别 | 字节 | 取值范围 | 默认值 |
---|---|---|---|---|
byte | 1 | -128(-2^7)---- 127(2^7-1) | 0 | |
short | 2 | -32768(-2^15)---- 32767(2^15 - 1) | 0 | |
int | 4 | -2,147,483,648(-231)**----**2,147,483,647(231 - 1) | 0 | |
long | 8 | (-263)**----**(263 -1) | 0 | |
float | 4 | 0.0f | ||
double | 8 | 0.0d | ||
char | 2 | 空 | ||
boolean | 1 | false |
2.关键字
2.1 Volatile
作用:
1)Volatile保证不同线程对共享变量操作的可见性,也就是说当一个线程修改了Volatile修饰的变量,当修改写回主内存时,其他线程可以看到最新的值。
2)禁止指令重排序
为了提高性能,编译器和处理器长长会对既定的代码执行顺序进行指令重排序。重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
总结:
- volatile修饰符适⽤于以下场景:某个属性被多个线程共享,其中有⼀个线程修改了此属性,其他线
程可以⽴即得到修改后的值,⽐如booleanflag;或者作为触发器,实现轻量级同步。- volatile属性的读写操作都是⽆锁的,它不能替代synchronized,因为它没有提供原⼦性和互斥性。
因为⽆锁,不需要花费时间在获取锁和释放锁_上,所以说它是低成本的。- volatile只能作⽤于属性,我们⽤volatile修饰属性,这样compilers就不会对这个属性做指令重排
序。- volatile提供了可⻅性,任何⼀个线程对其的修改将⽴⻢对其他线程可⻅,volatile属性不会被线程缓
存,始终从主 存中读取。- volatile提供了happens-before保证,对volatile变量v的写⼊happens-before所有其他线程后续对v的
读操作。- volatile可以使得long和double的赋值是原⼦的。
- volatile可以在单例双重检查中实现可⻅性和禁⽌指令重排序,从⽽保证安全性。
2.2Transient
作用:
将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会被序列化。
3.==和equals的区别
1.== 解读
对于基本类型和引用类型 == 的作用效果是不同的,如下所示:
基本类型:比较的是值是否相同;
引用类型:比较的是引用是否相同;
代码示例: String x = "string"; String y = "string"; String z = new String("string"); //jvm将其分配到堆内存 System.out.println(x==y); // true System.out.println(x==z); // false System.out.println(x.equals(y)); // true System.out.println(x.equals(z)); // true 解读因为 x 和 y 指向的是同一个引用,所以 == 也是 true,而 new String()方法则重写开辟了内存空间,所以 == 结果为 false,而 equals 比较的一直是值,所以结果都为 true。
2.equals
equals 本质上就是 ==,只不过 String 和 Integer 等重写了 equals 方法,把它变成了值比较。
equals源码解读
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } }
4.final,finally
- final 修饰类,表示类不可变,不能被继承
- final 修饰方法。表示该方法不可重写
- final 修饰变量,这个变量就是常量
5.方法重写和重载的区别
5.1重写(Override)
从字面上看,重写就是 重新写一遍的意思。其实就是在子类中把父类本身有的方法重新写一遍。子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名,参数列表,返回类型(除过子类中方法的返回值是父类中方法返回值的子类时)都相同的情况下, 对方法体进行修改或重写,这就是重写。但要注意子类函数的访问修饰权限不能少于父类的。
[代码演示]:
public class Father {
public static void main(String[] args) {
// TODO Auto-generated method stub
Son s = new Son();
s.sayHello();
}
public void sayHello() {
System.out.println("Hello");
}
}
class Son extends Father{
@Override
public void sayHello() {
// TODO Auto-generated method stub
System.out.println("hello by ");
}
}
总结:
- 发生在父类与子类之间
- 方法名,参数列表,返回类型(除过子类中方法的返回类型是父类中返回类型的子类)
- 访问修饰符的权限一定要大于被重写方法的访问修饰符
- 重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常
5.2重载(OverLoad)
在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同甚至是参数顺序不同)则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,但不能通过返回类型是否相同来判断重载。
[代码演示]:
public class Father {
public static void main(String[] args) {
// TODO Auto-generated method stub
Father s = new Father();
s.sayHello();
s.sayHello("wintershii");
}
public void sayHello() {
System.out.println("Hello");
}
public void sayHello(String name) {
System.out.println("Hello" + " " + name);
}
}
总结:
- 重载Overload是一个类中多态性的一种表现
- 重载要求同名方法的参数列表不同(参数类型,参数个数甚至是参数顺序)
- 重载的时候,返回值类型可以相同也可以不相同。无法以返回型别作为重载函数的区分标准
5.3面试时,问:重载(Overload)和重写(Override)的区别?
答:方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的参数列表,有兼容的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求,不能根据返回类型进行区分。
(三)锁
(四)String、StringBuffer、StringBuilder三者的区别
String和StringBuffer、StringBulider的区别在于String声明的是不可变的对象,每次操作都会生成新的String对象,然后将指针指向新的String对象,而StringBuffer、StringBuilder可以在原有对象的基础上操作。
StringBuffer和StringBulider最大的区别在于,StringBuffer是线程安全的,而String Builder是非线程安全的,但StringBuilder的性能却高于StringBuffer,所以在单线程环境下推荐使用StringBuilder;需要在线程安全的情况下推荐使用StringBuilder.
(五)序列化、反序列化
序列化与反序列化的定义:
(1)Java序列化就是指把Java对象转换为字节序列的过程
作用:在传递和保存对象时,保证对象的完整性和可传递性。对象转换为有序字节流时,便于在网络上传输或者存储在本地文件中。
(2)Java反序列化就是指把字节序列恢复为Java对象的过程
作用:根据字节流保存的对象状态以及描述信息,通过反序列化来重建对象。
实现序列化和反序列化的步骤
①实现序列化的必备要求:
实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列。
②JDK中序列化和反序列化的API:
java.io.ObjectInputStream:对象输入流。
该类的readObject()方法从输入流中读取字节序列,然后将字节序列反序列化为一个对象并返回。
java.io.ObjectOutputStream:对象输出流。
该类的writeObject(Object obj)方法将将传入的obj对象进行序列化,把得到的字节序列写入到目标输出流中进行输出。
[代码演示:]:
@Data
public Class Student implement Serializable{
//序列化id
private static final long serialVersionUID = -4060343097263809614L;
private String userName;
private String password;
private String age;
}
--------------------------------------------------------------------------------------
public class SerializableTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//序列化
FileOutputStream fos = new FileOutputStream("object.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
Student fsq = new Student("冯小猪", "5201314", "22");
oos.writeObject(fsq);
oos.flush();
//关闭资源
oos.close();
//反序列化
FileInputStream fis = new FileInputStream("object.out");
ObjectInputStream ois = new ObjectInputStream(fis);
Student pig = (Student) ois.readObject();
System.out.println(pig.getUserName()+ " " +pig.getPassword() + " " +pig.getAge());
}
}
二、JVM
1.JVM运行时内存区域
程序计数器:当CPU切换线程时,程序从上次执行位置开始(保存现场)
方法区(元空间):常量+静态变量+类信息
2JVM创建对象的步骤
第一步:虚拟机遇到一个new指令,首先去检查这个指令的参数能否在常量池中定位到这个类的符号引用,并且检查这个符号引用的类是否已经被加载&解析&初始化。
第二步:如果类已经被加载直接进行第三部;如果没有加载,就进行类的加载。
第三步:类加载检查通过后,给新对象分配内存空间。
第四步:对象生成需要的内存大小在类加载完成之后便可完全确认,为对象分配空间等同于把一块确定大小的内存从堆里划分出来。
第五步:内存大小的划分分为两种情况:
第一种情况:JVM的内存是规整的,所有的使用的内存都放到一边,空闲的内存在另外一边,中间放一个指针作为分界点的指示器。那么这时候分配内存就比较简单,只要讲指针向空闲空间那边挪动一段与对象大小相同的距离。这种就是“指针碰撞”。
第二种情况:JVM的内存不是规整的,也就是说已使用的内存与未使用的内存相互交错。这时候就没办法利用指正碰撞了。这时候我们就需要维护一张表,用于记录那些内存可用,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新到记录表上。
第六步:空间申请完成之后,JVM需要将内存的空间都初始化为0值。
第七步:JVM对对象进行必要的设置。例如:该对象是那个类的实例、对象的哈希码、GC年代信息等。
第八步:执行《init》方法,按照程序中设定的初始化操作来初始化对象。