牛客网部分面试题整理

这篇博客整理了Java面试中常见的一些关键字问题,包括final、Synchronized、volatile的使用及原理,以及static的相关知识点。深入探讨了这些关键字在多线程、内存可见性和代码执行顺序中的作用。此外,还涵盖了Java中的equals()和hashCode()方法的关系,以及面向对象的特性如重载和重写。
摘要由CSDN通过智能技术生成

一、关键字

(一)Java里面的final关键字是怎么用的?

final可以用来修饰类、类方法、以及变量

当用final修饰一个类时,表明这个类不能被继承;
修饰类方法时,则该方法不能被继承。也不能被重写;
修饰变量时,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。

(二)关于Synchronized和lock ?

  synchronized是Java的关键字,是内置的语言实现;当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。如果发生了异常,synchronized会自动释放线程占有的锁,因此不会导致死锁现象发生;
  Lock是一个接口,在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁。

此外,Lock是显式锁,需要手动开启和关闭锁,而synchronized是隐式锁,出了作用域自动释放;Lock只有代码块锁,而synchonized有代码块锁和方法锁。

(三)volatile

详细内容参考:https://www.cnblogs.com/dolphin0520/p/3920373.html

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修饰的成员可以通过set和get方法向外界提供访问方式
  • default(默认的)
    defalut即不写任何关键字,它可以修饰类,成员变量,成员方法,构造方法。被默认权限修饰后,其只能被本类以及同包下的其他类访问。
  • protected(受保护的)
    protected可以修饰成员变量,成员方法,构造方法,但不能修饰类(此处指的是外部类,内部类不加以考虑)。被protected修饰后,只能被同包下的其他类访问。如果不同包下的类要访问被protected修饰的成员,这个类必须是其子类。
  • public(公共的)
    public是权限最大的修饰符,他可以修饰类,成员变量,成员方法,构造方法。被public修饰后,可以再任何一个类中,不管同不同包,任意使用。

也就是:private在本类,defaule在本包,proetected在本包或者其他包中的子类,public任意包中的任意类

二、面向对象

面向对象的五大基本原则:
单一职责原则(Single-Resposibility Principle):一个类,最好只做一件事,只有一个引起它的变化。单一职责原则可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。
开放封闭原则(Open-Closed principle):软件实体应该是可扩展的,而不可修改的。也就是,对扩展开放,对修改封闭的。
Liskov替换原则(Liskov-Substituion Principle):子类必须能够替换其基类。这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。
依赖倒置原则(Dependecy-Inversion Principle):依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。
接口隔离原则(Interface-Segregation Principle):使用多个小的专门的接口,而不要使用一个大的总接口。

总结:
s( Single-Resposibility Principle ): 单一职责原则
o( Open-Closed 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,这个时候比较的就是两个对象的地址,结果当然是false,因为new了2个对象内存地址肯定不一样而重写之后,比较的就是两个对象的内容,结果是true。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值