面向对象
什么是面向对象?
对比面向过程,是两种不同的处理问题的角度。
面向过程更注重事情的每一个步骤及顺序,面向对象更注重事情有哪些参与者(对象)、及各自能做什么。
面向过程比较直接高效,而面向对象更易于复用、扩展和维护。
封装:内部细节对外部调用透明,外部调用无需修改或者关心内部实现。
继承:子类共性的方法或者属性直接使用父类的,不需要自己再定义,只需扩展自己的特性。
多态:父类引用指向子类对象。子类要对父类的方法进行重写,如果子类有的方法父类没有,那么父类就无法调用这个方法。
JDK、JRE、JVM三者区别和联系
JDK:Java开发工具
JRE:Java运行环境
JVM:Java虚拟机
==和equals
==对比的是栈中的值,基本数据类型是变量值,引用类型是堆中内存对象的地址。
equals:object中默认使用==比较,但是通常会重写。
final的作用
修饰类:表示类不可被继承
修饰方法:表示方法不可被子类覆盖,但是可以重载
修饰变量:表示变量一旦被赋值就不可再更改
如果final修饰的是类变量,只能在静态初始化块中指定初始值或者声明该类变量的时候指定初始值。
如果修饰的是成员变量,可以在非静态初始块、声明该变量或者构造器中执行初始值。
如果是局部变量,系统不会为局部变量进行初始化,局部变量必须由程序员初始化。因此使用final修饰局部变量时,即可以在定义时指定默认值(后面代码不能对变量再赋值),也可以不指定默认值,而在后面的代码中对final变量赋初值。
修饰基本类型数据和引用类型数据
如果是基本数据,其数值一旦在初始化之后便不能更改。
如果是引用类型,初始化之后不能再指向另一个对象,但是引用的值是可以变的。
为什么局部内部类和匿名内部类只能访问局部final变量?
编译之后会生成两个class文件,一个是Test.class,另一个是Test1.class。
内部类和外部类是处于同一个级别的,内部类不会因为定义在方法中,方法执行完就随之销毁。
这时候就会产生一个问题:当外部类的方法结束时,局部变量就会被销毁,但是此时内部类对象可能还引用着这个局部变量,就会导致内部类对象访问了一个不存在的变量。为了解决这个问题,就将局部变量复制了一份作为了内部类的成员变量,这样当局部变量死亡之后,内部类就仍然可以访问它,实际访问的就是局部变量的拷贝。这样就像是延长了局部变量的生命周期。
将局部变量复制为内部类的成员变量时,就必须要保证两个变量是一样的。因为可能外部类在执行代码的时候将局部变量的值改变。所以就将局部变量设置为final,对他初始化之后就不让再去修改这个变量,这样就保证了内部类的成员变量和方法的局部变量保持一致。
String StringBuffer StringBuilder区别及使用场景
String是final修饰的,不可变的,每次操作都会产生新的String对象。
StringBuffer和StringBuilder都是在原对象上进行操作的。
StringBuffer是线程安全的,StringBuilder是线程不安全的。
StringBuffer方法都是synchronized修饰的。
性能上:StringBuilder>StringBuffer>String
我们可以优先使用StringBuilder,多线程使用共享变量时使用StringBuffer。
重载和重写的区别
重载:发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符如果不同的话会导致编译错误。
重写:发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出异常的范围小于等于父类,访问修饰符范围大于等于父类。如果父类方法访问修饰符为private则子类就不能重写该方法。
抽象类和接口的区别
抽象类可以存在普通成员方法,而接口只能存在public abstract方法。
抽象类的成员变量可以使各种类型,而接口中的成员变量只能是public static final类型的。
抽象类只能继承一个,接口可以实现多个。
接口设计的目的是对类的行为进行约束,可以强制不同的类有相同的行为,但是他之约束了行为的有无,如何实现却没有进行限制。
抽象类设计的目的主要是代码复用。当不同的类具有某些相同的行为时,就可以吧相同点抽取出来形成抽象类,这样就达成了代码复用。
接口的核心是类可以做什么,抽象类的核心是代码复用。
List和set的区别
List:有序,是按对象进入的顺序来保存对象的,并且是可重复的。同时允许多个null元素对象,可以使用iterator取出所有元素,还可以使用下标来通过get方法获取元素。
set:无需,不可重复,最多允许有一个null元素对象,取元素时只能用iterator接口取得所有元素,没有提供下标获取元素。
hashcode和equals
hashCode()的作用是获取哈希码,它实际上是返回一个int整数。这个哈希码的作用是确定这个对象在哈希表中的索引位置。hashCode()定义在JDK的Object.java中。Java中的任何类都包含hashCode()方法。他可以根据key快速检索出对应的value,所以可以利用哈希码快速找到所需要的对象。
为什么要有hashCode?
对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,看该位置是否有值,如果没有、会让他加入进去,如果有值,这时候会调用equals方法来比对两个对象是否相同。如果两者相同,就不会加入成功,如果不同就重新计算散列到其他位置。这样就大大减少了equals的次数,相应就大大提高了执行速度。
如果两个对象相等,hashcode一定也相同。
两个对象相等,对两个对象调用equals方法返回true。
两个对象有相同的hashcode,他们不一定相等。
equals方法被覆盖过,则hashcode方法也必须被覆盖过。
hashcode()的默认行为是对堆上的对象产生独特值。如果没有重写hashcode方法,这个类的两个对象无论如何都不会相等。(即使他们指向相同的数据)
ArrayList和LinkedList的区别
ArrayList:底层是动态数组实现的,连续内存存储,适合下标访问(随机访问)。扩容:数组长度固定,超出长度存数据时需要新建数组,然后将老数组的数据拷贝到新数组,如果不是在尾部插入数据还会涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定合适的初始容量可以极大提升性能、甚至可能超过LinkedList(因为LinkedList需要创建大量对象)。
LinkedList:底层是链表实现,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询,查询时需要逐一遍历。
遍历LinkedList必须使用iterator不能使用for循环,因为每次for循环通过get(i)取得某一元素时都需要对list重新进行遍历,性能消耗极大。
使用indexOf返回元素索引时,如果结果为空,他会遍历整个列表。
HashMap和HashTable的区别?底层实现是什么?
区别:
HashMap方法没有synchronized修饰,不是线程安全的,HashTable有synchronized修饰,是线程安全的。
HashMap允许key和value为null,HashTable不允许。
底层实现:数组+链表
jdk8开始链表高度到8,数组长度超过64,链表转换为红黑树,元素以内部类Node节点存在。
计算key的hash值,二次hash然后对数组长度取模,对应到数组下标。
如果没有产生hash冲突(下标没有元素),就直接创建Node存入数组。
如果产生了hash冲突,就先进行equals比较,相同就取代该元素,不同就判断链表高度在插入链表,如果链表高度达到8并且数组长度到64就转变为红黑树。
key为null时,存入下标0的位置。
ConcurrentHashMap原理,jdk7和jdk8版本的区别
jdk7:
数据结构:ReentrantLock+Segment+HashEntry,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构。
锁:Segment分段锁继承了ReentrentLock,锁定操作的Segment,其他的Segment不受影响,并发度就是Segment的个数,可以通过构造函数指定,数组扩容不会影响其他的segment。
元素查询:要经过二次hash,第一次hash定位到哪个segment,第二次hash定位到元素所在数组的下标,也是链表的头部,剩下的和HashMap一致。
get方法是不需要加锁的,因为每个数组中的元素是一个node节点,node节点里的val存放要存的值,这个val被volatile修饰,从而保证了可见性,get方法也就不需要加锁了。
jdk8:
数据结构:因为在jdk8之后对sychronized进行了优化,所以一般来说性能要比ReentrentLock好了。
采用了sychronized+CAS+Node+红黑树,扩容或者hash冲突等场景会使用到sychronized。
查找,替换,赋值操作都使用的CAS。CAS是乐观锁,所以性能要比悲观锁高一点。
锁:锁的是链表的head节点,不影响其他元素的读写,锁的粒度更细,效率就更高,扩容时,阻塞所有的读写操作、进行并发扩容。
读的时候也是无锁的:
Node的val和next节点都使用了volatile进行修饰,读写线程对该变量相互可见。
数组也是用了volatile修饰,主要是因为数组扩容时能够被读线程立即感知到,不会读到脏数据。