java之语言

Posted muzinan110

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java之语言相关的知识,希望对你有一定的参考价值。

1.集合类

主要掌握如何实现.

ArrayList实现原理要点概括

ArrayList是List接口的可变数组非同步实现,并允许包括null在内的所有元素。
底层使用数组实现
该集合是可变长度数组,数组扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量增长大约是其容量的1.5倍,这种操作的代价很高。
采用了Fail-Fast机制,面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险
remove方法会让下标到数组末尾的元素向前移动一个单位,并把最后一位的值置空,方便GC

LinkedList实现原理要点概括
LinkedList是List接口的双向链表非同步实现,并允许包括null在内的所有元素。
底层的数据结构是基于双向链表的,该数据结构我们称为节点
双向链表节点对应的类Node的实例,Node中包含成员变量:prev,next,item。其中,prev是该节点的上一个节点,next是该节点的下一个节点,item是该节点所包含的值。
它的查找是分两半查找,先判断index是在链表的哪一半,然后再去对应区域查找,这样最多只要遍历链表的一半节点即可找到

HashMap实现原理要点概括
HashMap是基于哈希表的Map接口的非同步实现,允许使用null值和null键,但不保证映射的顺序。
底层使用数组实现,数组中每一项是个单向链表,即数组和链表的结合体;当链表长度大于一定阈值时,链表转换为红黑树,这样减少链表查询时间。
HashMap在底层将key-value当成一个整体进行处理,这个整体就是一个Node对象。HashMap底层采用一个Node[]数组来保存所有的key-value对,当需要存储一个Node对象时,会根据key的hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Node时,也会根据key的hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Node。
HashMap进行数组扩容需要重新计算扩容后每个元素在数组中的位置,很耗性能
采用了Fail-Fast机制,通过一个modCount值记录修改次数,对HashMap内容的修改都将增加这个值。迭代器初始化过程中会将这个值赋给迭代器的expectedModCount,在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map,马上抛出异常

Hashtable实现原理要点概括
Hashtable是基于哈希表的Map接口的同步实现,不允许使用null值和null键
底层使用数组实现,数组中每一项是个单链表,即数组和链表的结合体
Hashtable在底层将key-value当成一个整体进行处理,这个整体就是一个Entry对象。Hashtable底层采用一个Entry[]数组来保存所有的key-value对,当需要存储一个Entry对象时,会根据key的hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据key的hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。
synchronized是针对整张Hash表的,即每次锁住整张表让线程独占

ConcurrentHashMap实现原理要点概括
ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。
它使用了多个锁来控制对hash表的不同段进行的修改,每个段其实就是一个小的hashtable,它们有自己的锁。只要多个并发发生在不同的段上,它们就可以并发进行。
ConcurrentHashMap在底层将key-value当成一个整体进行处理,这个整体就是一个Entry对象。Hashtable底层采用一个Entry[]数组来保存所有的key-value对,当需要存储一个Entry对象时,会根据key的hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据key的hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。
与HashMap不同的是,ConcurrentHashMap使用多个子Hash表,也就是段(Segment)
ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。如果使用传统的技术,如HashMap中的实现,如果允许可以在hash链的中间添加或删除元素,读操作不加锁将得到不一致的数据。ConcurrentHashMap实现技术是保证HashEntry几乎是不可变的。

HashSet实现原理要点概括
HashSet由哈希表(实际上是一个HashMap实例)支持,不保证set的迭代顺序,并允许使用null元素。
基于HashMap实现,API也是对HashMap的行为进行了封装,可参考HashMap

LinkedHashMap实现原理要点概括
LinkedHashMap继承于HashMap,底层使用哈希表和双向链表来保存所有元素,并且它是非同步,允许使用null值和null键。
基本操作与父类HashMap相似,通过重写HashMap相关方法,重新定义了数组中保存的元素Entry,来实现自己的链接列表特性。该Entry除了保存当前对象的引用外,还保存了其上一个元素before和下一个元素after的引用,从而构成了双向链接列表。

LinkedHashSet实现原理要点概括
对于LinkedHashSet而言,它继承与HashSet、又基于LinkedHashMap来实现的。LinkedHashSet底层使用LinkedHashMap来保存所有元素,它继承与HashSet,其所有的方法操作上又与HashSet相同。

2.动态代理与反射
是java语言的特色,需要掌握动态代理与反射的使用场景.
ORM框架中会大量使用代理类,PRC调用时使用反射机制调用实现类的方法.

java的动态代理是用反射实现的。

什么是反射?
java的反射机制,是说在运行时刻,对于任何一个类,都能够知道它的所有属性和方法;对任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用方法的功能称为java的反射机制。

java通过java.lang.Class类实现反射机制。

Class类的getDeclaredMethod可以根据方法名字返回一个方法:

// 返回一个 Method 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明方法。
Method getDeclaredMethod(String name, Class<?>... parameterTypes)
1
2
Method的invoke方法可以执行这个方法:

// 对带有指定参数的指定对象调用由此 Method 对象表示的底层方法。
Object invoke(Object obj, Object... args)


3.数据类型
也是面试的常见问题.如每种数据类型占用多大空间,数据类型的自动转换与强制转换,基础数据类型与Wrapper数据类型的自动装箱与拆箱等.

一、基本数据类型:

byte:Java中最小的数据类型,在内存中占8位(bit),即1个字节,取值范围-128~127,默认值0
short:短整型,在内存中占16位,即2个字节,取值范围-32768~32717,默认值0
int:整型,用于存储整数,在内在中占32位,即4个字节,取值范围-2147483648~2147483647,默认值0
long:长整型,在内存中占64位,即8个字节-2^63~2^63-1,默认值0L
float:浮点型,在内存中占32位,即4个字节,用于存储带小数点的数字(与double的区别在于float类型有效小数点只有6~7位),默认值0
double:双精度浮点型,用于存储带有小数点的数字,在内存中占64位,即8个字节,默认值
char:字符型,用于存储单个字符,占16位,即2个字节,取值范围0~65535,默认值为空
boolean:布尔类型,占1个字节,用于判断真或假(仅有两个值,即true、false),默认值false

Java数据类型在内存中的存储:

1)基本数据类型的存储原理:所有的简单数据类型不存在“引用”的概念,基本数据类型都是直接存储在内存中的内存栈上的,数据本身的值就是存储在栈空间里面,而Java语言里面八种数据类型是这种存储模型;

2)引用类型的存储原理:引用类型继承于Object类(也是引用类型)都是按照Java里面存储对象的内存模型来进行数据存储的,使用Java内存堆和内存栈来进行这种类型的数据存储,简单地讲,“引用”是存储在有序的内存栈上的,而对象本身的值存储在内存堆上的;

区别:基本数据类型和引用类型的区别主要在于基本数据类型是分配在栈上的,而引用类型是分配在堆上的(需要java中的栈、堆概念),

基本类型和引用类型的内存模型本质上是不一样的。

例1:我们分析一下”==“和equals()的区别。

首先,我定以两个String对象

Stringa="abc";

Stringb="abc";

然后

if(a==b)

System.out.println("a==b");

else

System.out.println("a!=b");

程序输出a!=b

原因:a和b的地址是不相同的,a==b比较的是两个变量的地址

例2:定义两个基本类型

int a=4;

int b=4;

if(a==b)System.out.println("a==b");

else

System.out.println("a!=b");

输出:a==b

原因:==比较的是两个变量的内容

猜想:不论是基本数据类型还是引用类型,他们都会先在栈中分配一块内存,对于基本类型来说,这块区域包含的是基本类型的内容;而对于对象类型来说,这块区域包含的是指向真正内容的指针,真正的内容被手动的分配在堆上。


4.对象引用
(可自行搜索)

Java基础常考点–Map
能考查到数据结构,java的基础实现以及对并发问题的处理思路的掌握程度.
一、HashMap
通过数组加链表实现.
数组中的元素为一个链表,通过计算存入对象的hashcode,确认存入位置,用链表解决散列冲突.链表的节点存入的是键值对.

填充因子的作用
Map扩容的rehash机制
容量是二的幂次方.
是为了方便按位与操作计算余数,比求模更快
多线程风险的原因
对线程put时,会在超过填充因子的情况下rehash.HashMap为避免尾部遍历,链表插入采用头插法,多线程场景下可能产生死循环.

二、ConcurrentHashMap
分段锁思想
1.7中采用segment分段加锁,降低并发锁定程度.
CAS自旋锁
1.8中采用CAS自旋锁(一种乐观锁实现模式)提高性能.但在并发度较高时,性能一般.
红黑树
1.8引入红黑树解决hash冲突时的链表查找问题.在链表长度大于8且总容量大于64时启用.扩容后链表长度小于6时重新转为一般链表.(8,6,64为默认参数)

Java版本特性
1.8
* Lambda表达式
* StreamAPI
* 方法引用
* 接口默认方法
* Metaspace替换PremGen

1.9-1.10
* 模块系统
* 默认G1回收器
* 接口私有方法
* 局部变量判断
* Graal编译器

1.11
* ZGC
* 字符串API增强
* 内建HTTP Client


面试考察点
1.实现方法和使用方式
* HashMap在JDK1.8中的实现方式

扩容就是将旧数组的元素移动到新数组

1、HashMap底层是用数组+双向链表+红黑树实现的
2、插入元素的时候,首先通过一个hash方法计算得到key的哈希值,进而计算出待插入的位置
3、如果该位置为空,则直接插入(包装成Node)
4、如果该位置有值,则依次遍历。比较的规则是,hash值相同,key值相等的元素视为相同,则用新值替换旧值并返回旧值。
5、如果该位置的元素是红黑树结构,则同理,查找,找到则替换,没找到则插入。

划重点:
JDK1.8中HashMap与JDK1.7中有很多地方不一样
1、1.8中引入了红黑树,而1.7中没有
2、1.8中元素是插在链表的尾部,而1.7中新元素是插在链表的头部
3、扩容的时候,1.8中不会出现死循环,而1.7中容易出现死循环,而且链表不会倒置


2.实际应用中容易犯错的点
* ==与equals区别是什么

"=="和equals方法究竟有什么区别?
(单独把一个东西说清楚,然后再说清楚另一个,这样,它们的区别自然就出来了,混在一起说,则很难说清楚)

==操作符专门用来比较两个变量的值是否相等,也就是用于比较变量所对应的内存中所存储的数值是否相同,要比较两个基本类型的数据或两个引用变量是否相等,只能用==操作符。
如果一个变量指向的数据是对象类型的,那么,这时候涉及了两块内存,对象本身占用一块内存(堆内存),变量也占用一块内存,例如Objet obj = newObject();变量obj是一个内存,new Object()是另一个内存,此时,变量obj所对应的内存中存储的数值就是对象占用的那块内存的首地址。对于指向对象类型的变量,如果要比较两个变量是否指向同一个对象,即要看这两个变量所对应的内存中的数值是否相等,这时候就需要用==操作符进行比较。

equals方法是用于比较两个独立对象的内容是否相同,就好比去比较两个人的长相是否相同,它比较的两个对象是独立的。例如,对于下面的代码:
String a=new String("foo");
String b=new String("foo");

两条new语句创建了两个对象,然后用a/b这两个变量分别指向了其中一个对象,这是两个不同的对象,它们的首地址是不同的,即a和b中存储的数值是不相同的,所以,表达式a==b将返回false,而这两个对象中的内容是相同的,所以,表达式a.equals(b)将返回true。
在实际开发中,我们经常要比较传递进行来的字符串内容是否等,例如,String input = …;input.equals(“quit”),许多人稍不注意就使用==进行比较了,这是错误的,随便从网上找几个项目实战的教学视频看看,里面就有大量这样的错误。记住,字符串的比较基本上都是使用equals方法。
如果一个类没有自己定义equals方法,那么它将继承Object类的equals方法,Object类的equals方法的实现代码如下:
boolean equals(Object o)
return this==o;

这说明,如果一个类没有自己定义equals方法,它默认的equals方法(从Object类继承的)就是使用==操作符,也是在比较两个变量指向的对象是否是同一对象,这时候使用equals和使用==会得到同样的结果,如果比较的是两个独立的对象则总返回false。如果你编写的类希望能够比较该类创建的两个实例对象的内容是否相同,那么你必须覆盖equals方法,由你自己写代码来决定在什么情况即可认为两个对象的内容是相同的。
-----------------------------------------------------------------
简单的说,==比较两个人是否究竟是真正同一个人,equals一般用来比较两个人在逻辑上是否相等(比如规定两人成年之后身高相同就算两人相同等等),想怎么定义就怎么定义,如果不覆盖equals方法的话,默认仍然是比较两人是否同一个人(废话,两个人都还处于胚胎状态,都没有具体特征,怎么可能在逻辑上比较)。


* 对象强引用使用不当会导致内存泄露,考察不同引用方式和作用的理解
解决刚说的内存泄露问题,主要运用的就是显式声明引用的类型,首先简单解释下相关的知识:

 1、引用的类型:强,软,弱。普通的java引用,也是我们最一般的java引用对象,便是强引用,它直接影响垃圾回收的进行(有强引用指向对应的对象,GC便不可回收该对象,即使内存不够用);软引用,和强引用的主要区别是:它会影响GC对对象的回收工作,但是,当虚拟机的内存不够用时,软引用所指向对象(当然,不能同时有强引用指向对象)会被强制回收;弱引用就等级更低,它无法影响GC对对象的垃圾回收工作,只能引用并访问对象。具体三种引用可以由下面的代码例子说明:

//软引用和弱引用的使用例子
public void referenceExam()
Object object = new Object();//这个普通object引用就是一个强引用
//软引用通过SoftReference创建一个软引用
SoftReference<Object> softObjReference = new SoftReference<Object>(object);
//通过get方法获取真正的对象引用
softObjReference.get();
//弱引用通过WeakReference来关联一个对象
WeakReference<Object> weakObjReference = new WeakReference<Object>(object);
weakObjReference.get();//这个方法返回了真实的对象引用,当没有强引用或者弱引用指向它时,返回为null


  
2、对于防止内存泄露以及与垃圾回收有关的引用强弱类型,我们可以归纳出下面的特点:
  A、当我们在高速缓存中缓存较大的对象例如缓存图片等以提高读取速度的时候(针对大内存对象问题),可以将相关的对象设置为软引用对象,这样保证在内存不够用的时候GC可以进行回收工作。
  B、当我们完全不像由于当前对对象的引用而影响对象的其他模块原来设定的GC回收时期,我们可以采用弱引用(相当于只可访问数据而访问不会对GC对对象的回收造成影响)
  C、当然,java还有一种叫做虚引用的引用类型变量,该变量其实现实中用得不多,主要用来跟踪对象GC过程,所以这里不详细讲。
最后,至于什么时候要设置对象为弱引用(或者软引用),个人觉得还是要根据具体的业务进行对象使用的判断,如果全局对象过大,有可能在程序中造成泄露问题,或者说我们不想在不清楚对象何时可以被回收(工厂通过接口提供对象给客户)时,可以采用软引用或者弱引用进行对象访问。

 

以反例来描述实际场景中误用的危害.
如,大量使用反射会影响性能.


与知识点相关的最新技术趋势.
如,讲到ConcurrentHashMap,可以介绍1.8的改进细节.

 

真题汇总
一、Object中的equals和hashcode的作用分别是什么?
1 这两个方法都是来自java.lang.Object类,在Object中hashCode()返回的是对象的地址值,equals()方法是对两个对象的地址进行的比较;如果equals()方法的返回值相同,说明两个对象的地址值也是相同的,所以hashCode()的返回值也是相同的。

2在向集合(如HashSet,TreeSet等)中添加元素的时候遵循的规则是:
A 判断对象的hashCode的值是否相同,如果不相同,认为这两个元素不相同,如果相同,转入B。
B判断两个对象的equals运算的值是否相同,如果不相同,认为两个对象是不相同的,
如果相同认为两个对象是相同的。

3 在向封装类的对象中添加元素的时候,由于封装类已经重写了hashCode()和equals()方法,
所以这里使用的是封装类自己重写的两个方法。所以只要存入的内容相同就认为是相同的对象。

4 向集合中添加自定义类的对象时,如果自定义类没有重写equals()和hashCode()方法,就会调用Object的这两个方法,也就是hashCode()方法产生一个hashCode值(永远不会相同的地址),再使用equals()方法进行比较(地址);所以可能会产生向集合中添加已有的对象的时候,由于两次产生的hashcode值是不同的,equals()方法认为这两次添加的不是相同的元素,从而造成了集合对象中有重复的元素出现。解决的方法是:重写自定义类的这两个方法。

5 hashCode()方法经常和散列集合一起使用(HashSet,HashMap,HashTable),
如果集合中不允许有重复的元素,如果采用equals()进行比较的话,会产生效率问题。
所以使用hashCode()方法,在添加元素的时候就进行保证元素的唯一性。
6 如果一个类重写了equals()方法,则一定也要重写hashCode()方法,原因是:虽然equals()方法重写可以保证正确判断两个对象在逻辑是否相同,但是hashCode()方法映射的物理地址是不相同的,依然会将逻辑上相同的两个元素存入集合,但是第二个对象的内容会是Null.

二、final,finally,finalize的区别与使用场景
(1)final:可以作为修饰符修饰变量、方法和类,被final修饰的变量只能一次赋值;被final修饰的方法不能够在子类中被重写(override);被final修饰的类不能够被继承。
 (2)finally用在异常处理中定义总是执行代码,无论try块中的代码是否引发异常,catch是否匹配成功,finally块中的代码总是被执行,除非JVM被关闭(System.exit(1)),通常用作释放外部资源(不会被垃圾回收器回收的资源)。
 (3)finalize()方法是Object类中定义的方法,当垃圾回收器将无用对象从内存中清除时,该对象的finalize()方法被调用。由于该方法是protected方法,子类可以通过重写(override)该方法以整理资源或者执行其他的清理工作。(有可能对象会复活)


三、简单表述一下Java的异常机制
Java异常机制主要依赖于try、catch、finally、throw、throws五个关键字。

1.try:它里面放置可能引发异常的代码

2.catch:后面对应异常类型和一个代码块,用于表明该catch块用于处理这种类型的代码块,可以有多个catch块。

3.finally:主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件),异常机制总是保证finally块总是被执行。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者 throw等终止方法的语句,则就不会跳回执行,直接停止。

4.throw:用于抛出一个实际的异常,可以单独作为语句使用,抛出一个具体的异常对象。

5.throws:用在方法签名中,用于声明该方法可能抛出的异常。

 

Java的异常分为两种,checked异常(编译时异常)和Runtime异常(运行时异常)

1. java认为checked异常都是可以再编译阶段被处理的异常,所以它强制程序处理所有的checked异常,而Runtime异常无须处理,java程序必须显式处理checked异常,如果程序没有处理,则在编译时会发生错误,无法通过编译。

2. checked异常体现了java设计哲学:没有完善处理的代码根本不会被执行,体现了java的严谨性,


四、线上使用的那个版本jdk,为什么使用这个版本(有什么特色)?

 

以上是关于java之语言的主要内容,如果未能解决你的问题,请参考以下文章

java语言之输出数据的格式控制

数据结构之顺序表(Java语言描述)

Java语言核心技术之基本程序设计

Java进阶之reflection(反射机制)——反射概念与基础

Java进阶之reflection(反射机制)——反射概念与基础

笔记之_Java基础整理