⽜客⽹部分⾯试题整理
⼀、关键字
(⼀)Java⾥⾯的final关键字是怎么⽤的?
oppo强制解锁
final可以⽤来修饰类、类⽅法、以及变量
当⽤final修饰⼀个类时,表明这个类不能被继承;
修饰类⽅法时,则该⽅法不能被继承。也不能被重写;
修饰变量时,如果是基本数据类型的变量,则其数值⼀旦在初始化之后便不能更改;如果是引⽤类型的变量,则在对其初始化之后便不能再让其指向另⼀个对象。
(⼆)关于Synchronized和lock ?
synchronized是Java的关键字,是内置的语⾔实现;当它⽤来修饰⼀个⽅法或者⼀个代码块的时候,能够保证在同⼀时刻最多只有⼀个线程执⾏该段代码。如果发⽣了异常,synchronized会⾃动释放线程占有的锁,因此不会导致死锁现象发⽣;
Lock是⼀个接⼝,在发⽣异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使⽤Lock时需要在finally块中释放锁。周庄景点
此外,Lock是显式锁,需要⼿动开启和关闭锁,⽽synchronized是隐式锁,出了作⽤域⾃动释放;Lock只有代码块锁,⽽synchonized 有代码块锁和⽅法锁。
(三)volatile
1.介绍⼀下volatile?
volatile关键字与Java的内存模型有关,是⽤来保证程序的有序性和可见性的。
我们所写的代码,不⼀定是按照我们⾃⼰写的顺序来执⾏,因为编译器会做重排序。重排序过程不会影响到单线程程序的执⾏,却会影响到多线程并发执⾏的正确性。所以为了保证程序按写的顺序执⾏,就需要加volatile关键字,禁⽌重排序。⽽⼀旦⼀个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进⾏操作时的可见性,即⼀个线程修改了某个变量的值,这新值对其他线程来说是⽴即可见的。
2)禁⽌进⾏指令重排序。
烤大虾怎么做
那如何保证有序性呢?
是通过插⼊内存屏障来保证的。这⾥要知道happens-before 原则,其中有⼀条就是volatile变量规则:对⼀个变量的写操作先⾏发⽣于后⾯对这个变量的读操作。
如何保证可见性?
⾸先Java内存模型分为,主内存,⼯作内存。⽐如线程A从主内存把变量从主内存读到了⾃⼰的⼯作内存中,做了加1的操作,但是此时没有将i的最新值刷新回主内存中,线程B此时读到的还是i的旧值。⽽如果加了volatile关键字时,它能保证修改的值会⽴即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
PS:
1.重排序:处理器为了提⾼程序运⾏效率,可能会对输⼊代码进⾏优化,它不保证程序中各个语句的执⾏先后顺序同你写的代码中的顺序⼀致,但是它会保证程序最终结果和代码顺序执⾏的结果是⼀致的。重排序过程不会影响到单线程程序的执⾏,却会影响到多线程并发执⾏的正确性。
2.有序性:即程序执⾏的顺序按照代码的先后顺序执⾏
在java中可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有⼀个线程执⾏同步代码,相当于是让线程顺序执⾏同步代码,⾃然就保证了有序性。另外,Java内存模型具备⼀些先天的“有序性”,即不需要通过任何⼿段就能够得到保证的有序性,这个通常也称为 happens-before 原则。其中有⼀条就是volatile变量规则:对⼀个变量的写操作先⾏发⽣于后⾯对这个变量的读操作。
3.可见性:当多个线程访问同⼀个变量时,⼀个线程修改了这个变量的值,其他线程能够⽴即看得到修改的值。
当⼀个共享变量被volatile修饰时,它能保证修改的值会⽴即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
⽽普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写⼊主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此⽆法保证可见性。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同⼀时刻只有⼀个线程获取锁然后执⾏同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
4.加⼊volatile关键字的代码⽣成的汇编代码会多出⼀个lock前缀指令,lock前缀指令实际上相当于⼀个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后⾯的指令排到内存屏障之前的位置,也不会把前⾯的指令排到内存屏障的后⾯;即在执⾏到内存屏障这句指令时,在它前⾯的操作已经全部完成;
2)它会强制将对缓存的修改操作⽴即写⼊主存;
3)如果是写操作,它会导致其他CPU中对应的缓存⾏⽆效。
2.volatile关键字解决了什么问题?实现原理是什么?
解决了:
1.保证了变量的可见性
禁用词 2.禁⽌指令重排序
实现原理:
是内存屏障。作⽤是:
1)它确保指令重排序时不会把其后⾯的指令排到内存屏障之前的位置,也不会把前⾯的指令排到内存屏障的后⾯;即在执⾏到内存屏障这句指令时,在它前⾯的操作已经全部完成。总之⼀句话,它能保证指令按照我们希望的顺序执⾏;
2)它会强制将对缓存的修改操作⽴即写⼊主存(可见性);
3)如果是写操作,它会导致其他CPU中对应的缓存⾏⽆效。
对保险的认识
(四)static
1.static关键字是什么意思?Java中是否可以覆盖(override)⼀个是static的⽅法?
static关键字表明⼀个成员变量或者是成员⽅法可以在没有所属的类的实例变量的情况下被访问。
Java中static⽅法不能被覆盖,因为⽅法覆盖是基于运⾏时动态绑定的,⽽static⽅法是编译时静态绑定的。static⽅法跟类的任何实例都不相关,所以概念上不适⽤。
private只能够被⾃⾝类访问,⼦类不能访问private修饰的成员,所以也不能override⼀个private⽅法。
PS;静态绑定和动态绑定
在进⾏⽅法调⽤时,系统唯⼀的任务是确定被调⽤⽅法的版本。对于private、static、final⽅法或者构造器,这部分⽅法在程序真正运⾏之前就有⼀个可以确定的调⽤版本,并且该版本在运⾏期间是不可变的,编译器⼀开始就能确定要调⽤的版本,这叫做静态绑定,这些⽅法在类加载的时候就会把符号引⽤转化为该⽅法的直接引⽤。
与之对应,在程序运⾏期间确定⽅法调⽤版本的调⽤⽅式叫做动态绑定,此时,虚拟机会为每个类创建⼀个⽅法表,列出所有⽅法签名和实际调⽤的⽅法,这样⼀来虚拟机在调⽤⽅法时,只⽤查找该表就⾏了,只有在调⽤时采⽤动态绑定的⽅法才能体现出多态特性。
2.是否可以在static环境中访问⾮static变量?
不能。在静态中不能调⽤费静态,因为静态优先于⾮静态存在于内存中,当静态调⽤时,费静态还没有进⼊内存,没办法调⽤。
3.静态变量存在什么位置?
⽅法区
修饰符权限:
四⼤修饰符,分别为private,default,protected,public
private可以修饰成员变量,成员⽅法,构造⽅法,不能修饰类(此刻指的是外部类,内部类不加以考虑)。被private修饰的成员只能在其修饰的本类中访问,在其他类中不能调⽤,但是被private修饰的成员可以通过t和get⽅法向外界提供访问⽅式
default(默认的)
defalut即不写任何关键字,它可以修饰类,成员变量,成员⽅法,构造⽅法。被默认权限修饰后,其只能被本类以及同包下的其他类访问。
protected(受保护的)筋疲力尽造句
protected可以修饰成员变量,成员⽅法,构造⽅法,但不能修饰类(此处指的是外部类,内部类不加以考虑)。被protected修饰后,只能被同包下的其他类访问。如果不同包下的类要访问被protected修饰的成员,这个类必须是其⼦类。
public(公共的)
public是权限最⼤的修饰符,他可以修饰类,成员变量,成员⽅法,构造⽅法。被public修饰后,可以再任何⼀个类中,不管同不同包,任意使⽤。
也就是:private在本类,defaule在本包,proetected在本包或者其他包中的⼦类,public任意包中的任意类
⼆、⾯向对象
⾯向对象的五⼤基本原则:
单⼀职责原则(Single-Resposibility Principle):⼀个类,最好只做⼀件事,只有⼀个引起它的变化。单⼀职责原则可以看做是低耦合、⾼内聚在⾯向对象原则上的引申,将职责定义为引起变化的原因,以提⾼内聚性来减少引起变化的原因。
开放封闭原则(Open-Clod principle):软件实体应该是可扩展的,⽽不可修改的。也就是,对扩展开放,对修改封闭的。
Liskov替换原则(Liskov-Substituion Principle):⼦类必须能够替换其基类。这⼀思想体现为对继承机制的约束规范,只有⼦类能够替换基类时,才能保证系统在运⾏期内识别⼦类,这是保证继承复⽤的基础。
依赖倒置原则(Dependecy-Inversion Principle):依赖于抽象。具体⽽⾔就是⾼层模块不依赖于底层模块,⼆者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。
接⼝隔离原则(Interface-Segregation Principle):使⽤多个⼩的专门的接⼝,⽽不要使⽤⼀个⼤的总接⼝。
总结:
s( Single-Resposibility Principle ): 单⼀职责原则
o( Open-Clod principle ): 开放封闭原则
l( Liskov-Substituion Principle ): ⾥⽒原则
i( Interface-Segregation Principle ): 接⼝隔离原则
d( Dependecy-Inversion Principle ): 依赖倒置原则
⼀个单词:⽴⽅体(solid),很好记
(⼀)hashCode()和equals()相关
1.两个对象值相同(x.equals(y) == true),但却可有不同的hashcode,该说法是否正确,为什么?
答:不对,Java对象的eqauls⽅法和hashCode⽅法是这样规定的:
➀相等(相同)的对象必须具有相等的哈希码(或者散列码)。
➁hashCode()相等的两个对象他们的equal()不⼀定相等。标准入团申请书
此外equals()不相等的两个对象,hashcode()有可能相等(我的理解是由于哈希码在⽣成的时候产⽣冲突造成的)。反过来,hashcode()不等,⼀定能推出equals()也不等;hashcode()相等,equals()可能相等,也可能不等。
2.为什么重写equals?
重写equals⽅法的⽬的是判断两个对象的内容(内容可以有很多,⽐如同时⽐较姓名和年龄,同时相同的才是同⼀个对象)是否相同
如果不重写equals,那么默认⽐较的是对象的引⽤是否指向同⼀块内存地址,重写之后⽬的是为了⽐较两个对象的value值是否相等。(要注意的是,利⽤equals⽐较⼋⼤包装对象(如int,float等)和String类(因为该类已重写了equals和hashcode⽅法)对象时,默认⽐较的是值,在⽐较其它⾃定义对象时都是⽐较的引⽤地址。)
⽐如现在有两个Student对象:
Student s1=new Student("⼩明",18);祝福信息
Student s2=new Student("⼩明",18);
如果不重写equals,这个时候⽐较的就是两个对象的地址,结果当然是fal,因为new了2个对象内存地址肯定不⼀样⽽重写之后,⽐较的就是两个对象的内容,结果是true。
3.为什么重写equals还要重写hashcode?
在hashMap中,如果使⽤⾃定义对象作为HashMap的key来使⽤时,是先求出key的hashcode(),⽐较其值是否相等,若相等再⽐较equals(),若相等则认为他们是相等的。若equals()不相等则认为他们不相等。也就是说,只有两个都相等时,才能映射到同⼀个key!!
object中equals默认⽐较的是内存地址,hashcode默认⽐较的是内存地址的哈希值。如果equals重写了,⽐如说是基于对象的内容实现的,它为true时两个对象的内存地址并不⼀定相同(⽐如上⾯的两个student对象),这个时候,如果不重写hashcode,因为两个对象的内存地址不同,所以他们的hashcode值并不⼀定相同。就导致两个对象equals相等但是hashcode不相等。这样,当你⽤其中的⼀个对象作为键保存到hashMap、hashTable或hashSet中,再以“相等的”另⼀个作为键值去查找他们的时候,则根本找不到。(⼀个对象应⽤hashmap作为key时他们是先判断hashcode是否相等再⽐较equals,不相等就为不同的key)
HashMap⽤来判断key是否相等的⽅法,其实是调⽤了HashSet来判断加⼊的元素是否相等。重载hashCode()是为了对同⼀个key,能得到相同的HashCode,这样HashMap就可以定位到我们指定的key上。重载equals()是为了向HashMap表明当前对象和key上所保存的对象是相等的,这样我们才真正地获得了这个key所对应的这个键值对。
实例:
package Pool;
import HashMap;
public class Test {
public static void main(String []args){
HashMap<Person,String> map =new HashMap<Person, String>();
Person person1 =new Person(1234,"乔峰");
Person person2 =new Person(1234,"乔峰");
/
/判断equals是否相等
System.out.println("person1和person2是否相等: "+ person1.equals(person2));
//put到hashmap中去
map.put(person1,"天龙⼋部");
//get取出,从逻辑上讲应该能输出“天龙⼋部”
System.out.println("结果:"+(person2));
}
}
class Person{
int idCard;
String name;
public Person(int idCard, String name){
this.idCard = idCard;
this.name = name;
}
//重写equals
@Override
public boolean equals(Object obj){
if(this== obj){
return true;
}
if(obj ==null||getClass()!= Class()){
return fal;
}
Person person =(Person) obj;
//两个对象是否等值,通过idCard来确定
return this.idCard == person.idCard &&this.name == person.name;
}
}
尽管我们在进⾏get和put操作的时候,使⽤的key从逻辑上讲是等值的(通过equals⽐较是相等的),但由于没有重写hashCode⽅法,所以put操作时,key(person1的hashcode1)–>hash–>indexFor–>最终索引位置 ,⽽通过key取出value的时候 key(person2的hashcode2)–>hash–>indexFor–>最终索引位置,由于hashcode1不等于hashcode2,导致没有定位到⼀个数组位置⽽返回逻辑上错误的值null(也有可能碰巧定位到⼀个数组位置,但是也会判断其entry的hash值是否相等,上⾯get⽅法中有提到。)
所以,在重写equals⽅法的时候,必须注意重写hashCode⽅法,同时还要保证通过equals判断相等的两个对象,调⽤hashCode⽅法要返回同样的整数值。⽽如果equals判断不相等的两个对象,其hashCode可以相同(只不过会发⽣哈希冲突,应尽量避免)。
重写hashCode后: