Java面试问题大全——按这个背,妥了!(附答案)(持续更新中!)

一、Java基础


1、String类为什么是final的
 
  1. //附上String类的源码

  2. public final class String

  3. extends Object

  4. implements Serializable, Comparable<String>, CharSequence

AI写代码java运行

为什么String类是final的?主要是为了保持String是不可变的,因为被final修饰的类不能被继承,也就是说不能拥有自己的子类、不能被重写、需要进行初始化操作,所以String是final的保证了String的安全性和效率,因为在第二次给String赋值时不是在原地址上修改数据,而是重新指向一个对象,新地址,才有了字符串常量池

解释一下字符串常量池:创建字符串时,如果该字符串已经存在于池中,则将返回现有字符串的引用,而不是创建新对象。这时就会有多个String变量引用指向同一个地址的情况,这也是字符串的处理速度快的原因;如果字符串随时都可以被改变,那么改变一个字符串的值,对其进行引用的字符串就会错误,这样是很危险的,比如黑客直接改变数据库账号密码引用的字符串指向对象的值,就会造成安全漏洞。

因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享,不会因为线程安全问题而使用同步,字符串自己就是线程安全的。

因为字符串是不可变的,所以在它创建的时候HashCode就被缓存了,不需要重新计算,这就使字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象,这就是为什么HashMap中的键往往都使用字符串。而HashCode在java中经常配合基于散列的集合一起正常运行,这样的散列集合包括HashSet、HashMap以及HashTable,因为当向集合中插入对象时,是通过hashcode判别在集合中是否已存在该对象,不是通过equals方法低效率的逐个比较。

2、描述一下ArrayList和LinkedList各自实现和区别

List是接口类,ArrayList和LinkedList是List的实现类。

ArrayList是动态数组(顺序表)的数据结构,顺序表的存储地址是连续的所以查找比较快,但在插入和删除时由于需要把其它的元素顺序向后移动(或向前移动)所以比较耗时。

LinkedList是链表的数据结构,链表的存储地址是不连续的,每个存储地址通过指针指向,在查找时需要进行通过指针遍历元素所以在查找时比较慢,由于链表插入时不需移动其它元素所以在插入和删除时比较快。

ArrayList,LinkedList是不同步的,所以如果不要求线程安全的话可以使用ArrayList或LinkedList节省为同步而耗费的开销,也可以通过一些办法包装ArrayList,LinkedList使他们也达到同步,但效率可能会有所降低。

ArrayList的内部实现是基于基础的对象数组的,因此它使用get方法访问列表中的任意一个元素时速度要比LinkedList快;LinkedList中的get方法是按照顺序从列表的一端开始检查直到另外一端;对 LinkedList而言,访问列表中的某个指定元素没有更快的方法了。 

时间复杂度

假设有一个很大的列表,里面的元素已经排好序了,这个列表可能是ArrayList类型的也可能是LinkedList类型的,现在对这个列表来进行二分查找来比较分别是ArrayList和LinkedList时的查询速度,基本上ArrayList的时间要明显小于LinkedList的时间,因此在这种情况下不宜用LinkedList,二分查找法使用的随机访问策略,而LinkedList是不支持快速随机访问的,对一个LinkedList做随机访问所消耗的时间与这个list的大小是成比例的,而相应的在ArrayList中进行随机访问所消耗的时间是固定的

在某些情况 下LinkedList的表现要优于ArrayList,有些算法在LinkedList中实现时效率更高,如果有一个列表要对其进行大量的插入和删除操作,在这种情况下 LinkedList就是一个较好的选择,极端一点,重复的在一个列表的开端插入一个元素,当一个元素被加到ArrayList的最开端时,所有已经存在的元素都会后移,这就意味着数据移动和复制上的开销;相反的,将一个元素加到LinkedList的最开端只是简单的为这个元素分配一个记录,然后调整两个连接;在 LinkedList的开端增加一个元素的开销是固定的,而在ArrayList的开端增加一个元素的开销是与ArrayList的大小成比例的

空间复杂度 

 
 
  1. //在LinkedList中有一个私有的内部类,定义如下

  2. private static class Entry {

  3. Object element;

  4. Entry next;

  5. Entry previous;

  6. }

AI写代码java运行

每个Entry对象列表中有一个元素,同时还有在LinkedList中它的上一个元素和下一个元素,一个有1000个元素的LinkedList对象将有1000个连接在一起的Entry对象,每个对象都对应列表中的一个元素;这样的话,在一个LinkedList结构中将有一个很大的空间开销,因为它要存储这1000个Entity对象的相关信息。 

ArrayList使用一个内置的数组来存储元素,这个数组的起始容量是10,当数组需要增长时,新的容量按如下公式获得:新容量=(旧容量*3)/2+1,也就是说每一次容量大概会增长50%,这就意味着,如果有一个包含大量元素的ArrayList对象, 那么最终将有很大的空间会被浪费掉,这个浪费是由ArrayList的工作方式本身造成的。如果没有足够的空间来存放新的元素,数组将不得不被重新进行分 配以便能够增加新的元素。对数组进行重新分配,将会导致性能急剧下降。如果知道一个ArrayList将会有多少个元素,可以通过构造方法来指定容量,还可以通过trimToSize方法在ArrayList分配完毕之后去掉浪费掉的空间。 

总结一下

对ArrayList和LinkedList而言,在列表末尾增加一个元素所用的时间开销都是固定的,对 ArrayList而言主要是在内部数组中增加一项指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言这个开销是统一的,会分配一个内部Entry对象。 

在ArrayList中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的。 

LinkedList不支持高效的随机元素访问。 

ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间。

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案【点击此处即可/免费获取】https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho

3、反射中,Class.forName和classloader的区别

Java中的Class.forName(),classLoader,都可用来对类进行加载,Class.forName()除了会将.class文件加载到JVM之外,还会对类进行解释,执行类中的static静态代码块;而classLoader只干一件事,就是将.class加载到JVM中,并不会对static静态代码块中的内容进行解析,只有在new Instance()方法的时候才会对static进行解析。

Class.forName(String name)该方法内部调用的是:Class.forName(className, true, ClassLoader.getClassLoader(caller))

方法:Class.forName(String name, boolean initialize, ClassLoader loader)

参数name代表全限定类名;参数initialize表示是否初始化该类,为true是初始化该类;参数loader 对应的类加载器

Classloder.loaderClass(String name)其实该方法内部调用的是:Classloder. loadClass(name, false)

方法:Classloder. loadClass(String name, boolean resolve)

参数name代表类的全限定类名;参数resolve代表是否解析,resolve为true是解析该类 

两者的区别:Class.forName得到的class是已经初始化完成的,Classloder.loaderClass得到的class是还没有链接的。

4、说说Java集合类,list、set、queue、map实现类

在编程中经常需要保存多个数据,一般我们会想到数组,但数组的前提是需要明确的知到要保存的对象的数量,而一旦数组在初始化时指定了长度,那么这个长度就是不可变的,所以当我们需要保存一个可以动态增长的数据(也就是在编译时不知道具体会有多少数量),Java集合类就是一个很好的选择。

集合类主要负责保存、盛装其他数据,因此集合类也被称为容器类。所以的集合类都位于java.util包下,后来为了处理多线程环境下的并发安全问题,JDK1.5还在java.util.concurrent包下提供了一些多线程支持的集合类。

Collection:一组"对立"的元素,通常这些元素都服从某种规则

List必须保持元素特定的顺序

Set不能有重复元素

Queue保持一个队列(先进先出)的顺序

Map:一组成对的"键值对"对象

Collection和Map的区别在于容器中每个位置保存的元素个数:Collection 每个位置只能保存一个元素(对象);Map保存的是"键值对",就像一个小型数据库,可以通过"键"找到该键对应的"值"。

Set

Set继承Collection接口,不能包含重复元素,Set判断两个对象不是使用==来判断,是使用equals方法,新加入的元素会与已有的元素判断equals比较返回false则加入,否则拒绝加入,所以使用Set的时候有两点需要注意:放入的对象要实现equals方法;对set的构造函数中,传入的Collection参数不能包含重复的元素

HashSet

HashSet实现了Set接口,由哈希表提供支持,不保证Set的迭代顺序允许使用null值,同时不允许元素有重复,因为HashSet底层是使用HashMap来实现的,HashSet中的元素都存放在HashMap的key上面,value是一个统一的静态变量;

HashSet中添加元素调用add方法,然后会调用HashMap的put方法插入元素,HashMap的put方法插入元素时,会首先判断是否存在key,如果不存在,则插入这个key-value,存在则修改value值;在set中,value值没用,因此往HashSet中添加元素,首先判断key是否存在,不存在插入元素,存在则不做处理;

向HashSet中存入一个元素时,HashSet调用对象的HashCode方法获取对象的HashCode值,根据HashCode值决定对象的存储位置。HashSet判断元素对象是否相同的方法是同时使用HashCode和equals方法来判断;

HashSet判断元素相等方法时,首先判断两个对象的HashCode是否相等,如果不相等,则认为两个对象也不相等,如果相等再判断equals方法是否相等;如果hashCode相等,equals方法不相等,则认为时不同的对象;为什么这样做,主要是为了提高效率,HashCode的效率比equals效率更高,不必每次重新计算Hash值。

LinkedHashSet

linkedHashSet继承自HashSet,但和HashSet不同的是,它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的;当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素。 LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问Set里的全部元素时(遍历)将有很好的性能(链表很适合进行遍历)。

SortedSet

主要用于排序操作,实现此接口的子类都属于排序子类。

TreeSet

TreeSet是SortedSet接口的实现类,TreeSet可以确保集合元素处于排序状态,底层实现是通过二叉树,插入的元素要实现Comparable接口。

EnumSet

EnumSet是一个专门为枚举类设计的集合类,EnumSet中所有元素都必须是指定枚举类型的枚举值,该枚举类型在创建EnumSet时显式、或隐式地指定。EnumSet的集合元素也是有序的,它们以枚举值在Enum类内的定义顺序来决定集合元素的顺序。

List

List代表元素有序,可重复的集合,集合中每个元素都有对应的顺序索引,允许加入重复元素,通过索引指定元素的位置,默认按元素的添加顺序设置元素的索引。

ArrayList

ArrayList是基于数组实现的List类,它封装了一个动态的增长的、允许再分配的Object[]数组。

Vector

Vector和ArrayList在用法上几乎完全相同,但由于Vector是一个古老的集合,所以Vector提供了一些方法名很长的方法,但随着JDK1.2以后,java提供了系统的集合框架,就将Vector改为实现List接口,统一归入集合框架体系中.

Stack

Stack是Vector提供的一个子类,用于模拟"栈"这种数据结构(LIFO后进先出)。

LinkedList

实现List接口,能对它进行队列操作,即可以根据索引来随机访问集合中的元素。同时它还实现Deque接口,即能将LinkedList当作双端队列使用,自然也可以被当作"栈来使用"。

ArrayList和Vector最大的区别在于:ArrayList是非线程安全的,而Vector是线程安全的。当一个Iterator被创建并使用时,使用另一个线程修改Vector中的元素时,调用Iterator方法会抛出ConcurrentModificationException异常

 Queue

Queue用于模拟"队列"这种数据结构(先进先出 FIFO),新元素插入(offer)到队列的尾部,访问元素(poll)操作会返回队列头部的元素,队列不允许随机访问队列中的元素。

PriorityQueue

PriorityQueue并不是一个比较标准的队列实现,PriorityQueue保存队列元素的顺序并不是按照加入队列的顺序,而是按照队列元素的大小进行重新排序。

Deque

Deque接口代表一个"双端队列",双端队列可以同时从两端来添加、删除元素,因此Deque的实现类既可以当成队列使用、也可以当成栈使用。

ArrayDeque

是一个基于数组的双端队列,和ArrayList类似,它们的底层都采用一个动态的、可重分配的Object[]数组来存储集合元素,当集合元素超出该数组的容量时,系统会在底层重新分配一个Object[]数组来存储集合元素

Map

Map用于保存具有"映射关系"的数据,因此Map集合里保存着两组值,一组值用于保存Map里的key,另外一组值用于保存Map里的value。key和value都可以是任何引用类型的数据。Map的key不允许重复,即同一个Map对象的任何两个key通过equals方法比较结果总是返回false。关于Map,我们要从代码复用的角度去理解,java是先实现了Map,然后通过包装了一个所有value都为null的Map就实现了Set集合。

Map的这些实现类和子接口中key集的存储形式和Set集合完全相同(即key不能重复)

Map的这些实现类和子接口中value集的存储形式和List非常类似(即value可以重复、根据索引来查找)

HashMap

 HashMap根据键的HashCode值存储数据,具有很快的访问速度,遍历时,获取的元素顺序是随机的;允许null key存在,不支持线程的同步,即同一时刻有多个线程写map,可能导致最终数据不一致。如果需要同步可以用SynchroizedMap方法或者使用ConcurrentHashMap(基于ReentrantLock来实现的);HashMap是无序的,元素遍历的顺序和插入的顺序是不一致的,如果要一致可以使用下面的LinkedHashMap;和HashSet集合不能保证元素的顺序一样,HashMap也不能保证key-value对的顺序。并且类似于HashSet判断两个key是否相等的标准也是: 两个key通过equals()方法比较返回true、同时两个key的hashCode值也必须相等。

LinkedHashMap

LinkedHashMap也使用双向链表来维护key-value对的次序,该链表负责维护Map的迭代顺序,与key-value对的插入顺序一致(注意和TreeMap对所有的key-value进行排序进行区分)

HashTablb

是一个古老的Map实现类,在处理元素时使用Synchronize,所以它是线程安全的。

Properties 

Properties对象在处理属性文件时特别方便(windows平台上的.ini文件),Properties类可以把Map对象和属性文件关联起来,从而可以把Map对象中的key-value对写入到属性文件中,也可以把属性文件中的"属性名-属性值"加载到Map对象中。

SortedMap

正如Set接口派生出SortedSet子接口,SortedSet接口有一个TreeSet实现类一样,Map接口也派生出一个SortedMap子接口,SortedMap接口也有一个TreeMap实现类。

TreeMap

TreeMap就是一个红黑树数据结构,每个key-value对即作为红黑树的一个节点。TreeMap存储key-value对(节点)时,需要根据key对节点进行排序。TreeMap可以保证所有的key-value对处于有序状态。同样,TreeMap也有两种排序方式: 自然排序、定制排序。

WeakHashMap

WeakHashMap与HashMap的用法基本相似。区别在于,HashMap的key保留了对实际对象的"强引用",这意味着只要该HashMap对象不被销毁,该HashMap所引用的对象就不会被垃圾回收。但WeakHashMap的key只保留了对实际对象的弱引用,这意味着如果WeakHashMap对象的key所引用的对象没有被其他强引用变量所引用,则这些key所引用的对象可能被垃圾回收,当垃圾回收了该key所对应的实际对象之后,WeakHashMap也可能自动删除这些key所对应的key-value。

IdentityHashMap

IdentityHashMap的实现机制与HashMap基本相似,在IdentityHashMap中,当且仅当两个key严格相等(key1 == key2)时,IdentityHashMap才认为两个key相等。

EnumMap

EnumMap是一个与枚举类一起使用的Map实现,EnumMap中的所有key都必须是单个枚举类的枚举值。创建EnumMap时必须显式或隐式指定它对应的枚举类。EnumMap根据key的自然顺序(即枚举值在枚举类中的定义顺序) 。

5、HashMap的底层原理与实现结构

HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。
简单来说,HashMap由数组+链表组成的,数组是HashMap的主体链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度依然为O(1),因为最新的Entry会插入链表头部,仅需简单改变引用链即可,而对于查找操作来讲,此时就需要遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树。

构造hash表时,如果不指明初始大小,默认大小为16(即Node数组大小16),如果Node[]数组中的元素达到填充比*Node.length,通过resize()重新调整HashMap大小 变为原来2倍大小,扩容很耗时;当链表数组的容量超过初始容量的0.75时,再散列将链表数组扩大2倍,把原链表数组的搬移到新的数组中,这个值我们称为加载因子,因为提高空间利用率和 减少查询成本的折中,主要是泊松分布,0.75的话碰撞最小。

6、string、stringbuilder、stringbuffer区别

字符串广泛应用 在Java 编程中,在 Java 中字符串属于,Java 提供了 String 类来创建操作字符串,String的值是不可变的,这就导致每次对String的操作都会生成新的String对象,这样不仅效率低下,而且大量浪费有限的内存空间。

对字符串进行修改的时候,需要使用 StringBuffer 和 StringBuilder 类,和 String 类不同的是,StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象

StringBuffer 与 StringBuilder是字符缓冲变量,StringBuffer 与 StringBuilder 中的方法和功能完全是等价的,只是StringBuffer中的方法大都采用了synchronized 关键字进行修饰,因此是线程安全的,而StringBuilder没有这个修饰,可以被认为是线程不安全的。StringBuilder 是在JDK1.5才加入的。jdk的实现中StringBuffer与StringBuilder都继承自AbstractStringBuilder。

1、String类型的字符串对象是不可变的,一旦String对象创建后,包含在这个对象中的字符系列是不可以改变的,直到这个对象被销毁。

 2、StringBuilder和StringBuffer类型的字符串是可变的,不同的是StringBuffer类型的是线程安全的,而StringBuilder不是线程安全的。

3、如果是多线程环境下涉及到共享变量的插入和删除操作,StringBuffer则是首选。如果是非多线程操作并且有大量的字符串拼接,插入,删除操作则StringBuilder是首选。毕竟String类是通过创建临时变量来实现字符串拼接的,耗内存还效率不高,怎么说StringBuilder是通过JNI方式实现终极操作的。

4、StringBuilder和StringBuffer的“可变”特性总结如下:

(1)append,insert,delete方法最根本上都是调用System.arraycopy()这个方法来达到目的

(2)substring(int, int)方法是通过重新new String(value, start, end - start)的方式来达到目的。因此,在执行substring操作时,StringBuilder和String基本上没什么区别。

总的来说,三者在执行速度方面的比较:StringBuilder > StringBuffer > String。

1.使用String类的场景:在字符串不经常变化的场景中可以使用String类,例如常量的声明、少量的变量运算。

2.使用StringBuffer类的场景:在频繁进行字符串运算(如拼接、替换、删除等),并且运行在多线程环境中,则可以考虑使用StringBuffer,例如XML解析、HTTP参数解析和封装。

3.使用StringBuilder类的场景:在频繁进行字符串运算(如拼接、替换、和删除等),并且运行在单线程的环境中,则可以考虑使用StringBuilder,如SQL语句的拼装、JSON封装等。

7、HashTable和HashMap区别

1、继承的父类不同

Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。

2、线程安全性不同

javadoc中关于hashmap的一段描述如下:此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。

Hashtable 中的方法是Synchronize的,而HashMap中的方法在缺省情况下是非Synchronize的。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步,但使用HashMap时就必须要自己增加同步处理。(结构上的修改是指添加或删除一个或多个映射关系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用Collections.synchronizedMap方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,如下所示:

Map m = Collections.synchronizedMap(new HashMap(...));

AI写代码java运行

Hashtable 线程安全很好理解,因为它每个方法中都加入了Synchronize。

3、是否提供contains方法

HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,因为contains方法容易让人引起误解。

Hashtable则保留了contains,containsValue和containsKey三个方法,其中contains和containsValue功能相同。

4、key和value是否允许null值

其中key和value都是对象,并且不能包含重复key,但可以包含重复的value。

Hashtable中,key和value都不允许出现null值。但是如果在Hashtable中有类似put(null,null)的操作,编译同样可以通过,因为key和value都是Object类型,但运行时会抛出NullPointerException异常,这是JDK的规范规定的。

HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。

5、两个遍历方式的内部实现上不同

Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。

6、hash值不同

哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。

hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值。

Hashtable计算hash值,直接用key的hashCode(),而HashMap重新计算了key的hash值,Hashtable在求hash值对应的位置索引时,用取模运算,而HashMap在求位置索引时,则用与运算,且这里一般先用hash&0x7FFFFFFF后,再对length取模,&0x7FFFFFFF的目的是为了将负的hash值转化为正值,因为hash值有可能为负数,而&0x7FFFFFFF后,只有符号外改变,而后面的位都不变。

7、内部实现使用的数组初始化和扩容方式不同

HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。

Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。

Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增加的方式是 old*2+1。

8、String a= “abc” String b = "abc" String c = new String("abc") String d = "ab" + "c" .他们之间用 == 比较的结果
 
  1. String a = "abc";

  2. String b = "abc";

  3. String c = new String("abc");

  4. String d = "ab" + "c";

AI写代码java运行

显而易见的,我们知道String是final的,所以对象a和对象b是字符串常量池中的同一个字符串,所以a b 为 true;

但因为String是final的,所以new一个String对象c,那么 a c 和 b c 都为 false;

同样的,"ab" + "c"就是"abc",所以a b d 为true,简单总结一下就是第一个问题的字符串常量池知识点:创建字符串时,如果该字符串已经存在于池中,则将返回现有字符串的引用,而不是创建新对象。

9、String类中的方法

 篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案【点击此处即可/免费获取】https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho

10、抽象类和接口的区别

关于抽象类和接口的区别可以去我的文章Java -- 抽象类与接口进行详细的查看。


二、JavaIO

2、NIO

传统BIO是一种同步的阻塞IO,IO在进行读写时,该线程将被阻塞,线程无法进行其它操作。IO流在读取时,会阻塞。直到发生以下情况:1、有数据可以读取。2、数据读取完成。3、发生异常。以传统BIO模型为基础,通过线程池的方式维护所有的IO线程,实现相对高效的线程开销及管理。

NIO(JDK1.4)模型是一种同步非阻塞IO,主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(多路复用器)。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(多路复用器)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。

NIO和传统IO(一下简称IO)之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

NIO优点:

  1. 通过Channel注册到Selector上的状态来实现一种客户端与服务端的通信。
  2. Channel中数据的读取是通过Buffer , 一种非阻塞的读取方式。
  3. Selector 多路复用器 单线程模型, 线程的资源开销相对比较小。
  4. Channel(通道)。

Channel(通道)

传统IO操作对read()或write()方法的调用,可能会因为没有数据可读/可写而阻塞,直到有数据响应。也就是说读写数据的IO调用,可能会无限期的阻塞等待,效率依赖网络传输的速度。最重要的是在调用一个方法前,无法知道是否会被阻塞。

NIO的Channel抽象了一个重要特征就是可以通过配置它的阻塞行为,来实现非阻塞式的通道。Channel是一个双向通道,与传统IO操作只允许单向的读写不同的是,NIO的Channel允许在一个通道上进行读和写的操作。

  • FileChannel:文件
  • SocketChannel:
  • ServerSocketChannel:
  • DatagramChannel: UDP

Buffer(缓冲区)

Bufer顾名思义,它是一个缓冲区,实际上是一个容器,一个连续数组。Channel提供从文件、网络读取数据的渠道,但是读写的数据都必须经过Buffer。

Buffer缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该模块内存。为了理解Buffer的工作原理,需要熟悉它的三个属性:capacity、position和limit。

position和limit的含义取决于Buffer处在读模式还是写模式。不管Buffer处在什么模式,capacity的含义总是一样的。见下图:

capacity、position和limit

  • capacity:作为一个内存块,Buffer有固定的大小值,也叫作“capacity”,只能往其中写入capacity个byte、long、char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清楚数据)才能继续写数据。
  • position:当你写数据到Buffer中时,position表示当前的位置。出事的position值为0,当写入一个字节数据到Buffer中后,position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity-1。当读取数据时,也是从某个特定位置读,讲Buffer从写模式切换到读模式,position会被重置为0。当从Buffer的position处读取一个字节数据后,position向前移动到下一个可读的位置。
  • limit:在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)

Buffer的分配:

对Buffer对象的操作必须首先进行分配,Buffer提供一个allocate(int capacity)方法分配一个指定字节大小的对象。向Buffer中写数据:写数据到Buffer中有两种方式:

1.从channel写到Buffer

int bytes = channel.read(buf); //将channel中的数据读取到buf中

AI写代码

2.通过Buffer的put()方法写到Buffer

buf.put(byte); //将数据通过put()方法写入到buf中

AI写代码

  • flip()方法:将Buffer从写模式切换到读模式,调用flip()方法会将position设置为0,并将limit设置为之前的position的值。
    从Buffer中读数据:从Buffer中读数据有两种方式:

1.从Buffer读取数据到Channel

int bytes = channel.write(buf); //将buf中的数据读取到channel中

AI写代码

2.通过Buffer的get()方法读取数据

byte bt = buf.get(); //从buf中读取一个byte

AI写代码

  • rewind()方法:Buffer.rewind()方法将position设置为0,使得可以重读Buffer中的所有数据,limit保持不变。
  • clear()与compact()方法:一旦读完Buffer中的数据,需要让Buffer准备好再次被写入,可以通过clear()或compact()方法完成。如果调用的是clear()方法,position将被设置为0,limit设置为capacity的值。但是Buffer并未被清空,只是通过这些标记告诉我们可以从哪里开始往Buffer中写入多少数据。如果Buffer中还有一些未读的数据,调用clear()方法将被"遗忘 "。compact()方法将所有未读的数据拷贝到Buffer起始处,然后将position设置到最后一个未读元素的后面,limit属性依然设置为capacity。可以使得Buffer中的未读数据还可以在后续中被使用。
  • mark()与reset()方法:通过调用Buffer.mark()方法可以标记一个特定的position,之后可以通过调用Buffer.reset()恢复到这个position上。

Selector(多路复用器)

Selector与Channel是相互配合使用的,将Channel注册在Selector上之后,才可以正确的使用Selector,但此时Channel必须为非阻塞模式。Selector可以监听Channel的四种状态(Connect、Accept、Read、Write),当监听到某一Channel的某个状态时,才允许对Channel进行相应的操作。

  • Connect:某一个客户端连接成功后
  • Accept:准备好进行连接
  • Read:可读
  • Write:可写
3、String 编码UTF-8 和GBK的区别
  • UTF8编码格式很强大,支持所有国家的语言,正是因为它的强大,才会导致它占用的空间大小要比GBK大,对于网站打开速度而言,也是有一定影响的。
  • GBK编码格式,它的功能少,仅限于中文字符,当然它所占用的空间大小会随着它的功能而减少,打开网页的速度比较快。

Java中UTF-8转GBK之所以不会出现中文乱码,是因为UTF-8编码为兼容性最大的字符集编码,它本身就支持中文字符。在Java开发中,特别是web开发,乱码是一种很常见而且很头疼的问题,这常常是由于页面端、服务端、数据库等几处所使用的字符不一致所致,故开发中,保持编码一致, 往往能减少由于乱码而带来的时间浪费,是一件非常重要的事情。

4、什么时候使用字节流、什么时候使用字符流

在程序中所有的数据都是以流的方式进行传输或保存的,程序需要数据的时候要使用输入流读取数据,而当程序需要将一些数据保存起来的时候,就要使用输出流完成。

字符流处理的单元为2个字节的Unicode字符,操作字符、字符数组或字符串,字节流处理单元为1个字节,操作字节和字节数组;所以字符流是由Java虚拟机将字节转化为2个字节的Unicode字符为单位的字符而成的,所以它对多国语言支持性比较好!如果是音频文件、图片、歌曲,就用字节流好点,如果是关系到中文(文本)的,用字符流好点。

如果是读写字符数据的时候则使用字符流,如果读写的数据都不需要转换成字符的时候,则使用字节流。

5、递归读取文件夹下的文件,代码怎么实现
 
  1. /**

  2. * 递归读取文件夹下的 所有文件

  3. *

  4. * @param testFileDir 文件名或目录名

  5. */

  6. private static void testLoopOutAllFileName(String testFileDir) {

  7. if (testFileDir == null) {

  8. //因为new File(null)会空指针异常,所以要判断下

  9. return;

  10. }

  11. File[] testFile = new File(testFileDir).listFiles();

  12. if (testFile == null) {

  13. return;

  14. }

  15. for (File file : testFile) {

  16. if (file.isFile()) {

  17. System.out.println(file.getName());

  18. } else if (file.isDirectory()) {

  19. System.out.println("-------this is a directory, and its files are as follows:-------");

  20. testLoopOutAllFileName(file.getPath());

  21. } else {

  22. System.out.println("文件读入有误!");

  23. }

  24. }

  25. }

AI写代码java运行


三、JavaWeb 


1、session和cookie的区别和联系

cookie 和session 的区别:

  • cookie数据存放在客户的浏览器上,session数据放在服务器上。
  • cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗,考虑到安全应当使用session。
  • session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用COOKIE。
  • 单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。

cookie 和session 的联系:

  • session是通过cookie来工作的
  • session和cookie之间是通过$_COOKIE['PHPSESSID']来联系的,通过$_COOKIE['PHPSESSID']可以知道session的id,从而获取到其他的信息。
2、传统JDBC连接方式
  • 注册驱动(驱动程序管理类DriverManager)
  • 建立连接(数据库连接类Connection)
  • 创建statement(声明类Statement)
  • 执行sql,得到ResultSet(结果集合类ResultSet)
  • 查看结果
  • 释放资源
 
  1. import java.sql.DriverManager;

  2. import java.sql.ResultSet;

  3. import java.sql.SQLException;

  4. import java.sql.Statement;

  5. import java.sql.Connection;

  6. import java.sql.DriverManager;

  7. import java.sql.SQLException;

  8. public class javaTest {

  9. public static void main(String[] args) throws ClassNotFoundException, SQLException {

  10. String URL="jdbc:mysql://127.0.0.1:3306/imooc?useUnicode=true&characterEncoding=utf-8";

  11. String USER="root";

  12. String PASSWORD="tiger";

  13. //1.加载驱动程序

  14. Class.forName("com.mysql.jdbc.Driver");

  15. //2.获得数据库链接

  16. Connection conn=DriverManager.getConnection(URL, USER, PASSWORD);

  17. //3.通过数据库的连接操作数据库,实现增删改查(使用Statement类)

  18. Statement st=conn.createStatement();

  19. ResultSet rs=st.executeQuery("select * from user");

  20. //4.处理数据库的返回结果(使用ResultSet类)

  21. while(rs.next()){

  22. System.out.println(rs.getString("user_name")+" "

  23. +rs.getString("user_password"));

  24. }

  25. //关闭资源

  26. rs.close();

  27. st.close();

  28. conn.close();

  29. }

  30. }

AI写代码java运行


 篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案【点击此处即可/免费获取】https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho

四、JVM 


1、Java的内存模型

程序计数器

程序计数器是众多编程语言都共有的一部分,作用是标示下一条需要执行的指令的位置,分支、循环、跳转、异常处理、线程恢复等基础功能都是依赖程序计数器完成的。

  对于Java的多线程程序而言,不同的线程都是通过轮流获得cpu的时间片运行的,这符合计算机组成原理的基本概念,因此不同的线程之间需要不停的获得运行,挂起等待运行,所以各线程之间的计数器互不影响,独立存储。这些数据区属于线程私有的内存。

Java虚拟机栈

VM虚拟机栈也是线程私有的,生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法调用直至执行完的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

  有人将java内存区域划分为栈与堆两部分,在这种粗略的划分下,栈标示的就是当前讲的虚拟机栈,或者是虚拟机栈对应的局部变量表。之所以说这种划分比较粗略是角度不同,这种划分方法关心的是新申请内存的存在空间,而我们目前谈论的是JVM整体的内存划分,由于角度不同,所以划分的方法不同,没有对与错。

  局部变量表存放了编译期可知的各种基本类型,对象引用,和returnAddress。其中64位长的long和double占用了2个局部变量空间(slot),其他类型都占用1个。这也从存储的角度上说明了long与double本质上的非原子性。局部变量表所需的内存在编译期间完成分配,当进入一个方法时,这个方法在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。

  由于栈帧的进出栈,显而易见的带来了空间分配上的问题。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlowError异常;如果虚拟机栈可以扩展,扩展时无法申请到足够的内存,将会抛出OutOfMemoryError。显然,这种情况大多数是由于循环调用与递归带来的。

本地方法栈

本地方法栈与虚拟机栈的作用十分类似,不过本地方法是为native方法服务的。部分虚拟机(比如 Sun HotSpot虚拟机)直接将本地方法栈与虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会抛出StactOverFlowError与OutOfMemoryError异常。

Java堆

Java堆是虚拟机所管理的内存中最大的一块,在虚拟机启动时创建,此块内存的唯一目的就是存放对象实例,几乎所有的对象实例都在对上分配内存。JVM规范中的描述是:所有的对象实例以及数据都要在堆上分配。但是随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配(对象只存在于某方法中,不会逃逸出去,因此方法出栈后就会销毁,此时对象可以在栈上分配,方便销毁),标量替换(新对象拥有的属性可以由现有对象替换拼凑而成,就没必要真正生成这个对象)等优化技术带来了一些变化,目前并非所有的对象都在堆上分配了。

  当java堆上没有内存完成实例分配,并且堆大小也无法扩展是,将会抛出OutOfMemoryError异常。Java堆是垃圾收集器管理的主要区域。

方法区

方法区与java堆一样,是线程共享的数据区,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译的代码。JVM规范将方法与堆区分开,但是HotSpot将方法区作为永久代(Permanent Generation)实现。这样方便将GC分代手机方法扩展至方法区,HotSpot的垃圾收集器可以像管理Java堆一样管理方法区。但是这种方向已经逐步在被HotSpot替换中,在JDK1.7的版本中,已经把原本存放在方法区的字符串常量区移出。

运行时常量池

运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等信息外,还有一项信息是常量池(Constant Poll Table)用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放。

  其中字符串常量池属于运行时常量池的一部分,不过在HotSpot虚拟机中,JDK1.7将字符串常量池移到了java堆中

2、GC算法

首先我们了解一下什么是GC:

GC垃圾收集,Java提供的GC可以自动监测对象是否超过作用域从而达到自动回收内存的目的。

垃圾回收可有效使用内存和防止内存泄露。垃圾回收器通常是作为一个单独的低优先级线程运行,不可预知的情况下对内存堆中已死亡或长久无使用的对象进行清除和回收。

回收机制:分代复制垃圾回收、标记垃圾回收、增量垃圾回收等方式。

JVM的内存空间,从大的层面上来分析包含:新生代空间(Young)和老年代空间(Old)。新生代空间(Young)又被分为2个部分(Eden区域、Survivous区域)和3个板块(1个Eden区域和2个Survivous区域)

一般来说是这样的:

我们来了解一下这些概念:

新生代,顾名思义,主要是用来存放新生的对象。新生代又细分为 Eden区、SurvivorFrom区、SurvivorTo区。

新创建的对象都会被分配到Eden区(如果该对象占用内存非常大,则直接分配到老年代区), 当Eden区内存不够的时候就会触发MinorGC(Survivor满不会引发MinorGC,而是将对象移动到老年代中),在Minor GC开始的时候,对象只会存在于Eden区和Survivor from区,Survivor to区是空的。

Minor GC操作后,Eden区如果仍然存活(判断的标准是被引用了,通过GC root进行可达性判断)的对象,将会被移到Survivor To区。而From区中,对象在Survivor区中每熬过一次Minor GC,年龄就会+1岁,当年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置,默认是15)的对象会被移动到年老代中,否则对象会被复制到“To”区。经过这次GC后,Eden区和From区已经被清空。

“From”区和“To”区互换角色,原Survivor To成为下一次GC时的Survivor From区, 总之,GC后,都会保证Survivor To区是空的。

奇怪为什么有 From和To,2块区域?这就要说到新生代Minor GC的算法了:复制算法。把内存区域分为两块,每次使用一块,GC的时候把一块中的内容移动到另一块中,原始内存中的对象就可以被回收了,优点是避免内存碎片。

老年代,随着Minor GC的持续进行,老年代中对象也会持续增长,导致老年代的空间也会不够用,最终会执行Major GC(MajorGC 的速度比 Minor GC 慢很多很多,据说10倍左右)。Major GC使用的算法是:标记清除(回收)算法或者标记压缩算法。

标记清除(回收):首先会从GC root进行遍历,把可达对象(存过的对象)打标记;再从GC root二次遍历,将没有被打上标记的对象清除掉。

优点:老年代对象一般是比较稳定的,相比复制算法,不需要复制大量对象。之所以将所有对象扫描2次,看似比较消耗时间,其实不然,是节省了时间。举个栗子,数组 1,2,3,4,5,6。删除2,3,4,如果每次删除一个数字,那么5,6要移动3次,如果删除1次,那么5,6只需移动1次。

缺点:这种方式需要中断其他线程(STW),相比复制算法,可能产生内存碎片。

标记压缩:和标记清除算法基本相同,不同的就是,在清除完成之后,会把存活的对象向内存的一边进行压缩,这样就可以解决内存碎片问题。 

当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。

永久代(元空间),在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间,Metaspace)的区域所取代。

值得注意的是:元空间并不在虚拟机中,而是使用本地内存(之前,永久代是在jvm中)。这样,解决了以前永久代的OOM问题,元数据和class对象存在永久代中,容易出现性能问题和内存溢出,毕竟是和老年代共享堆空间。java8后,永久代升级为元空间独立后,也降低了老年代GC的复杂度。

接下来我们说一下四大GC算法:

引用计数算法(Reference counting)

每个对象在创建的时候,就给这个对象绑定一个计数器。每当有一个引用指向该对象时,计数器加一;每当有一个指向它的引用被删除时,计数器减一。这样,当没有引用指向该对象时,该对象死亡,计数器为0,这时就应该对这个对象进行垃圾回收操作。为每个对象额外存储一个计数器 RC ,根据 RC 的值来判断对象是否死亡,从而判断是否执行 GC 操作。

初始状态

改变引用后

 优点:

  • 简单
  • 计算代价分散
  • “幽灵时间”短(幽灵时间指对象死亡到回收的这段时间,处于幽灵状态)

缺点:

  • 不全面(容易漏掉循环引用的对象)
  • 并发支持较弱
  • 占用额外内存空间

标记-清除算法

最基础的垃圾收集算法是“标记-清除”(Mark Sweep)算法,正如名字一样,算法分为2个阶段:1.标记处需要回收的对象,2.回收被标记的对象。标记算法分为两种:1.引用计数算法(Reference Counting) 2.可达性分析算法(Reachability Analysis)。由于引用技术算法无法解决循环引用的问题,所以这里使用的标记算法均为可达性分析算法。

如图所示,当进行过标记清除算法之后,出现了大量的非连续内存。当java堆需要分配一段连续的内存给一个新对象时,发现虽然内存清理出了很多的空闲,但是仍然需要继续清理以满足“连续空间”的要求。所以说,这种方法比较基础,效率也比较低下。

优点:

  • 最大的优点是,相比于引用计数法,标记—清除算法中每个活着的对象的引用只需要找到一个即可,找到一个就可以判断它为活的。
  • 此外,这个算法相比于引用计数法更全面,在指针操作上也没有太多的花销。更重要的是,这个算法并不移动对象的位置(后面俩算法涉及到移动位置的问题)。

缺点:

  • 很长的幽灵时间,判断对象已经死亡,消耗了很多时间,这样从对象死亡到对象被回收之间的时间过长。
  • 每个活着的对象都要在标记阶段遍历一遍;所有对象都要在清除阶段扫描一遍,因此算法复杂度较高。
  • 没有移动对象,导致可能出现很多碎片空间无法利用的情况。

复制算法

为了解决效率与内存碎片问题,复制(Copying)算法出现了,它将内存划分为两块相等的大小,每次使用一块,当这一块用完了,就讲还存活的对象复制到另外一块内存区域中,然后将当前内存空间一次性清理掉。这样的对整个半区进行回收,分配时按照顺序从内存顶端依次分配,这种实现简单,运行高效。不过这种算法将原有的内存空间减少为实际的一半,代价比较高。

从图中可以看出,整理后的内存十分规整,但是白白浪费一般的内存成本太高。然而这其实是很重要的一个收集算法,因为现在的商业虚拟机都采用这种算法来回收新生代。IBM公司的专门研究表明,新生代中的对象98%都是“朝生夕死”的,所以不需要按照1:1的比例来划分内存。HotSpot虚拟机将Java堆划分为年轻代(Young Generation)、老年代(Tenured Generation),其中年轻代又分为一块Eden和两块Survivor。

所有的新建对象都放在年轻代中,年轻代使用的GC算法就是复制算法。其中Eden与Survivor的内存大小比例为8:2,其中Eden由1大块组成,Survivor由2小块组成。每次使用内存为1Eden+1Survivor,即90%的内存。由于年轻代中的对象生命周期往往很短,所以当需要进行GC的时候就将当前90%中存活的对象复制到另外一块Survivor中,原来的Eden与Survivor将被清空。但是这就有一个问题,我们无法保证每次年轻代GC后存活的对象都不高于10%。所以在当活下来的对象高于10%的时候,这部分对象将由Tenured进行担保,即无法复制到Survivor中的对象将移动到老年代。

优点:

  • 实现简单
  • 不产生内存碎片

缺点:

  • 每次运行,总有一半内存是空的,导致可使用的内存空间只有原来的一半。

标记-整理算法

复制算法在极端情况下(存活对象较多)效率变得很低,并且需要有额外的空间进行分配担保。所以在老年代中这种情况一般是不适合的。

所以就出现了标记-整理(Mark-Compact)算法。与标记清除算法一样,首先是标记对象,然而第二步是将存货的对象向内存一段移动,整理出一块较大的连续内存空间。

优点:

  • 该算法不会像标记-清除算法那样产生大量的碎片空间。

缺点:

  • 如果存活的对象过多,整理阶段将会执行较多复制操作,导致算法效率降低。

复制算法与标记-整理算法的区别在于,复制算法不是在同一个区域复制,而是将所有存活的对象复制到另一个区域内。

  • 不同算法有不同的优点和缺点,除了引用计数法不常用外,其他三种算法在现在的java虚拟机上也是很常见的,间接说明了这几个经典算法还是有其适用性的。
  • Java堆分为年轻代有年老代,其中年轻代分为1个Eden与2个Survior,同时只有1个Eden与1个Survior处于使用中状态,又有年轻代的对象生存时间为往往很短,因此使用复制算法进行垃圾回收。
  • 年老代由于对象存活期比较长,并且没有可担保的数据区,所以往往使用标记-清除与标记-整理算法进行垃圾回收。

**阿里面经问题

 篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案【点击此处即可/免费获取】https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho


1、spring的原理和核心

IOC、DI、AOP

IOC(Inversion of Control)控制反转

简单地说,由spring来负责控制对象的生命周期和对象间的关系。传统的Java SE程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IOC是有专门一个容器来创建这些对象,即由Ioc容器来控制对象的创建;就是把new对象实例化的工作交给spring容器来完成,spring帮我们负责销毁对象,控制对象的生命周期,在需要使用对象的时候直接向spring申请即可。控制对象生存周期的不再是引用它的对象,而是spring。对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被spring控制,所以这叫控制反转。

DI(Dependency Injection)依赖注入

组件之间依赖关系由容器在运行期决定,由容器动态的将某个依赖关系注入到组件之中。在系统运行中,动态的向某个对象提供它所需要的其他对象。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。Java 1.3之后一个重要特征是反射(reflection),它允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性,spring就是通过反射来实现注入的。


注入方式:set方式注入、构造器注入、工厂方法注入,注解方式注入

set方式注入:目标对象中需要提供相关的set方法,需要调用set方法将资源传递给目标对象使用。

构造器注入:目标对象中提供带参数的构造方法,通过构造方法将资源传递给目标对象使用。

静态工厂注入:调用静态工厂的方法来获取自己需要的对象。

实例工厂注入:实例工厂的意思是获取对象实例的方法不是静态的,所以你需要首先new工厂类,再调用普通的实例方法

注解方式注入:Spring2.5之后,Spring增加了注解注入。@Autowired 注解,可以对Bean类成员变量、方法及构造函数进行标注,完成依赖注入的自动装配工作。使用@Autowired可以省略Bean类的待依赖注入对象的set方法。@Resource注解的功能和@Autowired注解功能相近,@Resource有name和type两个主要的属性。Spring容器对于@Resource注解的name属性解析为bean的名字,type属性则解析为bean的类型。因此使用name属性,则按byName模式的自动注入策略,如果使用type属性则按 byType模式自动注入策略。如果两个属性都未指定,Spring容器将通过反射技术默认按byName模式注入。

AOP(Aspect-OrientedProgramming)面向切面

纵向重复的代码横向抽取,使用过滤器 Filter

在面向对象编程(oop)思想中,我们将事物纵向抽成一个个的对象。而在面向切面编程中,我们将一个个的对象某些类似的方面横向抽成一个切面,对这个切面进行一些如权限控制、事物管理,记录日志等公用操作处理的过程就是面向切面编程的思想。

2、一个页面从输入 URL 到页面加载显示完成,这个过程中都发生了什么

输入URL、DNS转换、HTTP服务器请求、服务器处理请求、网站后台处理、浏览器解析渲染、断开链接

在浏览器输入url后,浏览器并不能直接通过url找到服务器,而是要通过ip地址

DNS作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。而不用去记住能够被机器直接读取的IP数串。通过主机名,最终得到该主机名对应的IP地址的过程叫做域名解析(或主机名解析)

HTTP 请求分为三个部分:TCP 三次握手、http 请求响应信息、关闭 TCP 连接

第一次握手:建立连接,发送包到服务器,等待服务器确认;

第二次握手:服务器收到包,必须确认客户的包,同时自己也发送一个包;

第三次握手:客户端(浏览器)收到服务器的包,向服务器发送确认包,完成三次握手;

三次握手结束后,开始发送 HTTP 请求报文

服务器对于不同用户发送的请求,会结合配置文件,把不同请求委托给服务器上处理对应请求的程序进行处理(例如CGI脚本,JSP脚本,servlets,ASP脚本,服务器端JavaScript,或者一些其它的服务器端技术等),然后返回后台程序处理产生的结果作为响应

网站处理,就是实际后台处理的工作。后台开发现在有很多框架,大部分都还是按照MVC设计模式进行搭建的。MVC是Model(模型)、View(视图)和Controller(控制)

通过后台处理返回的html字符串结果会被浏览器读取解析,对应就是html页面加载、解析、渲染的工作

当数据传送完毕,需要断开 tcp 连接,此时发起 tcp 四次挥手

TCP客户端发送一个FIN,用来关闭客户到服务器的数据传送;

服务器收到这个FIN,它发回一个ACK,确认序号为收到的序号加1;

服务器关闭客户端的连接,发送一个FIN给客户端;

客户端发回ACK报文确认,并将确认序号设置为收到序号加1;

3、POST和GET的区别

GET提交的数据放在URL中,POST则不会,这是最显而易见的差别。这点意味着GET更不安全(POST也不安全,因为HTTP是明文传输抓包就能获取数据内容,要想安全还得加密);

GET回退浏览器无害,POST会再次提交请求(GET方法回退后浏览器再缓存中拿结果,POST每次都会创建新资源);

GET提交的数据大小有限制(是因为浏览器对URL的长度有限制,GET本身没有限制),POST没有;

GET可以被保存为书签(BookMark),POST不可以;

GET会被浏览器主动cache,而POST不会,除非手动设置;

GET只允许ASCII字符,POST没有限制;

GET只能进行url编码,而POST支持多种编码方式;

GET会保存在浏览器历史记录中,POST不会;

GET产生一个TCP数据包;POST产生两个TCP数据包

对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);

对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据);

在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点;

并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次

4、转发和重定向的区别

Forward和Redirect代表了两种请求转发方式:直接转发和间接转发。

直接转发方式(Forward),客户端和浏览器只发出一次请求,Servlet、HTML、JSP或其它信息资源,由第二个信息资源响应该请求,在请求对象request中,保存的对象对于每个信息资源是共享的;

地址栏不发生变化,显示的是上一个页面的地址

请求次数:只有1次请求

根目录:http://localhost:8080/项目地址/,包含了项目的访问地址

请求域中数据不会丢失

间接转发方式(Redirect)实际是两次HTTP请求,服务器端在响应第一次请求的时候,让浏览器再向另外一个URL发出请求,从而达到转发的目的;

地址栏:显示新的地址

请求次数:2次

根目录:http://localhost:8080/ ,没有项目的名字

请求域中的数据会丢失,因为是2次请求

5、为什么要使用自定义的异常类

Java虽然提供了丰富的异常处理类,但是在项目中还会经常使用自定义异常,其主要原因是Java提供的异常类在某些情况下还是不能满足实际需球。例如以下情况:

系统中有些错误是符合Java语法,但不符合业务逻辑;

在分层的软件结构中,通常是在表现层统一对系统其他层次的异常进行捕获处理;

6、红黑树的特性

每个结点是黑色或者红色。

根结点是黑色。

每个叶子结点(NIL)是黑色。 [注意:这里叶子结点,是指为空(NIL或NULL)的叶子结点!]

如果一个结点是红色的,则它的子结点必须是黑色的。

每个结点到叶子结点NIL所经过的黑色结点的个数一样的。[确保没有一条路径会比其他路径长出俩倍,所以红黑树是相对接近平衡的二叉树的!]

具体的红黑树操作以及左旋右旋,变色这些操作,可以参考文章

7、如何在100 亿URL中判断某个URL是否存在 

问题描述:如果现在有一台电脑接收了1亿个url,现在又有一个url过来,如何在短时间内确定这个url之前有没有来过?

解决方法:布隆过滤器

当输入一个 url 的时候,此时这个 url 会经过 k 个哈希函数处理,得到多个哈希值(v1,v2,...,vk)之后分别将这些哈希值除以数组的长度 m,和对 m 取模,得到这些哈希值对应在数组的下标位置,最后将这些下标的元素都置为 1;

这是再来一个url时,它经过上述处理之后,会得到多个数组的下标位置,如果这些下标的元素值都已经为 1 了,说明该在黑名单里面,否则不在;

但是这样有一个缺点就是,已经存在的数据是绝对可以查找到的,但是不存在的数据也有可能会被认为时存在过的,具有一定的失误率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值