Java面试---JAVA基础

目录

一、JAVA中的几种基本数据类型是什么,各自占用多少字节。

二、String 类能被继承吗,为什么

三、String,StringBuffer,StringBuilder的区别

四、ArrayList、Vector 和 LinkedList 有什么区别

五、讲讲类的实例化顺序,比如父类静态数据,构造函数,字段,子类静态数据,构造函数,字段,当new的时候,他们的执行顺序。

六、用过哪些 Map类,都有什么区别,HashMap是线程安全的吗,并发下使用的 Map是什么,他们内部原理分别是什么,比如存储方式,hashcode,扩容,默认容量等。

七、Java8 的 ConcurrentHashMap 为什么放弃了分段锁,有什么问题吗,如果你来设计,你如何设计

八、有序的 Map实现类有哪些,他们是怎么保证有序的。

九、抽象类和接口的区别,类可以继承多个类么,接口可以继承多个接口么,类可以实现多个接口么

十、继承和聚合的区别在哪

十一、IO模型有哪些,讲讲你理解的 NIO ,他和 BIO,AIO的区别是啥,谈谈 Reactor模型。

十二、反射的原理,反射创建类实例的三种方式是什么

十三、反射中,Class.forName和 ClassLoader区别

十四、描述动态代理的几种实现方式,分别说出相应的优缺点

十五、动态代理与CGlib实现的区别

十六、为什么 CGlib方式可以对接口实现代理

十七、final 的用途

十八、写出三种单例模式实现

十九、如何在父类中为子类自动完成所有的 hashcode 和 equals实现?这么做有何优劣

二十、请结合OO设计理念,谈谈访问修饰符public、private、protected、default在应用设计中的作用

二十一、深拷贝和浅拷贝区别

二十二、数组和链表数据结构描述,各自的时间复杂度

二十三、Error 和 Exception的区别,CheckedException,RuntimeException的区别

二十四、请列出5个运行时异常。

二十五、在自己的代码中,如果创建一个 java.lang.String类,这个类是否可以被类加载器加载?为什么

二十六、说一说你对 java.lang.Object对象中 hashCode和 equals方法的理解。在什么场景下需要重新实现这两个方法

二十七、在 jdk1.5中,引入了泛型,泛型的存在是用来解决什么问题

二十八、这样的 a.hashcode() 有什么用,与 a.equals(b)有什么关系

二十九、有没有可能2个不相等的对象有相同的 hashcode

三十、Java 中的 HashSet 内部是如何工作的

三十、什么是序列化,怎么序列化,为什么序列化,反序列化会遇到什么问题,如何解决

三十二、java8 的新特性

三十三、sort() 底层使用的是什么算法

三十四、常见算法的复杂度是多少

三十五、int 与 Integer 之间的数据比较

三十六、函数式编程与面向对象编程的区别

三十七、equals 和 ==区别, 重写 equals一定要重写 hashcode方法吗?为什么? hashcode方法有什么作用?

三十八、Java序列化,有ID和没ID会出现问题吗


一、JAVA中的几种基本数据类型是什么,各自占用多少字节。


Java语言中一共提供了8种原始的数据类型(byte,short,int,long,float,double,char,boolean),这些数据类型不是对象,而是 Java语言中不同于类的特殊类型,这些基本类型的数据变量在声明之后就会立刻在栈上被分配内存空间。除了这8种基本的数据类型外,其他类型都是引用类型(例如类、接口、数组等),引用类型类似于C++中的引用或指针的概念,它以特殊的方式指向对象实体,此类变量在声明时不会被分配内存空间,只是存储了一个内存地址而已。

数据类型 字节长度 范围 默认值 包装类
int 4 (-2^31~2^31-1) 0 Integer
short 2 [-32768,32767] 0 Short
byte 1 [-128,127] 0 Byte
long 8 (-2^63~2^63-1) 0L或0l Long
double 8 64位IEEE754双精度范围 0 Double
float 4 32位IEEE754单精度范围 0.0F或0.0f Float
char 2 Unicode [0,65535] u0000 Character
boolean 1 true和false flase Boolean

二、String 类能被继承吗,为什么


不可以,因为 String类有 final修饰符,而 final修饰的类是不能被继承的,实现细节也不允许改变。

三、String,StringBuffer,StringBuilder的区别


【1】String是不可变类,String对象一旦被创建,其值就不能改变,而 StringBuffer是可变类,当对象被创建后仍然可以对其值进行修改。由于 String是不可变类,因此适合在被共享的场合中使用,而当一个字符串经常被修改时,最好使用 StringBuffer来实现。如果用 String保存一个经常修改的字符串时,字符串被修改时会比 StringBuffer多很多附加的操作,同时生成很多无用的对象,由于这些无用的对象会被垃圾回收器来回收,因此会影响程序的性能。在规模小的项目里面这个影响很小,但是在一个规模大的项目里面,这会对程序的运行效率带来很大的影响。
【2】String 与 StringBuffer实例化时存在区别:String 可以通过构造函数的方式(String s = new String("hello"))和直接赋值(String s="world")两种方式。而 StringBuffer只能使用构造函数进行赋值(StringBuffer sb = new StringBuffer("hello"))。
【3】String 字符串修改实现的原理:当 String修改字符串时,先创建一个 StringBuffer,其次调用 append()方法,最后调用toString()方法把结果返回。实例如下(下述过程比使用 StringBuffer多了一些附加操作,同时也生成了一些临时的对象,从而导致程序执行效率下降):

String s = "HELLO";
s+="WORLD";
//以上代码 实现底层 如下
StringBuffer sb = new StringBuffer(s);
sb.append("WORLD");
s=sb.toString();
【4】StringBuilder 也是可以被修改的字符串,他与 StringBuffer类似,都是字符缓冲区,但 StringBuild不是线程安全的,如果只是单线程访问时可以使用 StringBuilder,当有多个线程访问时,最好使用线程安全的 StringBuffer。因为 StringBuffer必要时会对这些方法进行同步,所以任意特定实例上的所有操作就好像是以串行顺序发生的,该顺序与所涉及的每个线程进行的方法调用顺序一致。
【5】在执行效率方面:StringBuilder 最高,StringBuffer 次之,String 最低,鉴于以上情况,一般使用数据量较小的情况下,优先使用 String;如果单线程下使用大量数据,应优先使用 StringBuilder类;如果是在多线程下操作大量数据,应优先考虑StringBuffer类。

四、ArrayList、Vector 和 LinkedList 有什么区别


ArrayList、Vecotr、LinkedList 类均为 java.util 包中,均为可伸缩数据,既可以动态改变长度的数组。都是 list 接口的实现类;

【1】ArrayList 和 Vector都是基于存储的 Object[] array 来实现的,它们会在内存中开辟一块连续的空间来存储(默认是10数组大小的内存),由于数据存储时连续的,因此,它们支持用序号(下标)来访问元素,同时索引数据的速度比较快。但是在插入/删除元素时需要移动容器中的元素,所以对数据的插入/删除操作执行比较慢。ArrayList 和 Vector都是一个初始化的容量的大小,当存储的元素超过分配内存大小时就需要动态地扩展它们的存储空间(会重新创建一个新的数组,将旧的数据复制过去)。为了提高效率,每次扩充容量时,不是简单的扩充一个存储单位,而是一次增加多个存储单元,Vector 默认扩充为原来的2倍(每次扩充的大小是可以设置的),而 ArrayList 默认扩充1.5倍(没有提供方法来设置空间扩充大小)。
【2】ArrayList 与 Vector最大的区别就是 synchronization(同步)的使用,没有一个 ArrayList的方法是同步,而 Vector的绝大数方法(add、insert、remove、set、equals、hashcode等)都是直接或者间接同步的,所以 Vector是线程安全的,ArrayList 不是线程安全的。正是由于 Vector是线程安全的,所以性能上也略逊于 ArrayList。
【3】LinkedList 是采用双向列表来实现的,对数据的索引需要从列表头开始遍历,因此用于随机访问效率比较低,但是插入元素时不需要对数据进行移动,因此插入效率高。同时 LinkedList是非线程安全的容器。
【4】那么,在实际使用时,当对数据主要操作为索引或只是集合的未端增加、删除元素时,使用 ArralyList或 Vector效率比较高;当对数据的操作主要是指位置的插入或者删除操作时,使用 LinkedList效率比较高;当在线程中使用容器时(既多线程同时访问该容器),选用 Vector较为安全。

五、讲讲类的实例化顺序,比如父类静态数据,构造函数,字段,子类静态数据,构造函数,字段,当new的时候,他们的执行顺序。


类加载机制是什么
一个.java文件在编译后会形成相应的一个或多个Class文件(若一个类中含有内部类,则编译后会产生多个Class文件),但这些Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能被运行和使用。事实上,虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程就是虚拟机的 类加载机制 。

类加载器实例化时进行的操作步骤(加载【类的加载,指类加载器根据查找路径找到并类的.class文件读入内存,并为之创建一个java.lang.Class对象】—>连接【1、验证:检查.class文件内部结构的正确性。2、准备:为类的静态变量分配内存并设置默认初始值,这些变量所使用的内存都将在方法区中进行分配。3、解析:把类中的符号引用转换为直接引用) —>初始化【对静态变量显示初始化和静态代码块执行初始化。】)。所有的 Java虚拟机实例必须在每个类或接口被 Java程序“首次主动使用”时才初始化它们。
执行顺序:父类静态代变量、父类静态代码块、子类静态变量、子类静态代码块、父类非静态变量(父类实例成员变量)、父类构造函数、子类非静态变量(子类实例成员变量)、子类构造函数。

六、用过哪些 Map类,都有什么区别,HashMap是线程安全的吗,并发下使用的 Map是什么,他们内部原理分别是什么,比如存储方式,hashcode,扩容,默认容量等。


使用过的Map类:【1】HashMap:最常用的 Map,它根据键的 HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。它是线程不安全的Map,方法上都没有synchronize关键字修饰。HashMap允许空(null)键值,但最多只允许一条记录的键为null
【2】HashTable 是线程安全的 Map实现类,它实现线程安全的方法是在各个方法上添加了synchronize关键字,但是现在已经不再推荐使用 HashTable了,因为现在有了 ConcurrentHashMap这个专门用于多线程(并发)场景下的 Map实现类,其大大优化了多线程下的性能。
【3】ConcurrentHashMap:如果你经常参加面试,一定会被问到这个 Map实现类,这个 Map实现类是在 jdk1.5中加入的,其在 jdk1.6/1.7中的主要实现原理是 segment分段锁,而每个Segment 都继承了 ReentrantLock 类,也就是说每个Segment类本身就是一个锁。使用 put 方法的时候,是对我们的 key进行 hash拿到一个整型,然后将整型对16取模,拿到对应的Segment,之后调用 Segment的 put方法,然后上锁,这里lock()的时候其实是 this.lock(),也就是说,每个 Segment的锁是分开的。它不再使用和 HashTable一样的 synchronize一样的关键字对整个方法进行加锁,而是转而利用segment 段落锁来对其进行加锁,以保证 Map的多线程安全。其实可以理解为,一个 ConcurrentHashMap 是由多个 HashTable组成,所以它允许获取到不同段锁的线程同时持有该资源,也就是说 segment有多少个,理论上就可以同时有多少个线程来持有它这个资源。其默认的 segment是一个数组,默认长度为16。也就是说理论上可以提高16倍的性能。在 Java 的 jdk1.8中则对ConcurrentHashMap又再次进行了大的修改,取消了 segment段锁字段,采用了CAS+Synchronize技术来保障线程安全。具体7中有介绍。
【4】TreeMap:是一个很常用的 Map实现类,因为他具有一个很大的特点就是会对 Key进行排序,使用了 TreeMap存储键值对,再使用 Iterator进行输出时,会发现其默认采用 key由小到大的顺序输出键值对,如果想要按照其他的方式来排序,需要重写也就是 override 它的 compartor 接口。TreeMap 底层的存储结构也是一颗红黑树。红黑树查找效率高,只有O(logn)。它是一种自平衡的二叉查找树。在每次插入和删除节点时,都可以自动调节树结构,以保证树的高度是logn。

 public class Compare {
     public static void main(String[] args) {
     TreeMap map = new TreeMap(new xbComparator());
         map.put("key_1", 1);
         map.put("key_2", 2);
         map.put("key_3", 3);   
         Set keys = map.keySet();
         Iterator iter = keys.iterator();
         while(iter.hasNext()){
             String key = iter.next();
             System.out.println(" "+key+":"+map.get(key));
         }
     }
 }
     //重写排序方法
     class xbComparator implements Comparator{
         public int compare(Object o1,Object o2){
         String i1=(String)o1;
         String i2=(String)o2;
         return -i1.compareTo(i2);
     }
 }


【5】LinkedHashMap:它的特点主要在于 Linked,带有这个字眼的就表示底层用的是链表来进行的存储。相对于其他的无序的Map实现类,还有像 TreeMap这样的排序类,LinkedHashMap最大的特点在于有序,但是它的有序主要体现在先进先出FIFIO上。没错,LinkedHashMap主要依靠双向链表和 hash表来实现的。
【6】WeakHashMap:与 HashMap类似,二者不同之处在于 WeakHashMap中的 key不再被外部引用,它就可以被垃圾回收器回收。而 HashMap中 key采用的是“强引用的方式”,当 HashMap中的 key没有被外部引用时,只有在这个 key从 HashMap中删除后,才可以被垃圾回收器回收。

七、Java8 的 ConcurrentHashMap 为什么放弃了分段锁,有什么问题吗,如果你来设计,你如何设计


ConcurrentHashMap 分段锁中存在一个分段锁个数的问题,既 Segment[] 的数组长度。当长度设置小了,数据结构根据额外的竞争,从而导致线程试图写入当前锁定的段,导致阻塞。相反,如果高估了并发级别,当遇到过大的膨胀(大量的并发),由于段产生的不必要数量,这种膨胀会导致性能的下降。因为高速缓存未命中。而 Java8中仅仅是为了兼容旧版本而保留。唯一的作用就是保证构造 Map时初始容量不小于 concurrencyLevel。

在 Java的 jdk1.8中则对 ConcurrentHashMap采用 CAS+Synchronize(取代 Segment+ReentrantLock)技术来保障线程安全底层采用数组+链表+红黑树[当链表长度为8时,使用红黑树]的存储结构,也就是和 HashMap一样。这里注意 Node其实就是保存一个键值对的最基本对象。其中 value和 next都是使用的 volatile关键字进行了修饰,以确保线程安全。

volatile修改变量后,此变量就具有可见性,一旦该变量修改,其他线程立马就会知道,立马放弃自己在自己工作内存中持有的该变量值。转而重主内存中获取该变量最新的值。

在插入元素时,会首先进行 CAS判定,如果 OK就是插入其中,并将 size+1,但是如果失败了,就会通过自旋锁自旋后再次尝试插入,直到成功。所谓 CAS也就是 Compare And Swap,既在更改前先对内存中的变量值和你指定的那个变量值进行比较,如果相同就说明再次期间没有被修改,而如果不一样了,则就要停止修改,否则就会影响到其他人的修改,将其覆盖掉。举例:内存值a,旧值b,和要修改后的值c,如果这里a=b,那么就可以进行更改,就可以将内存值a=c。否则就要终止该更新操作。如果链表中存储的Entry超过了8个则就会自动转换链表为红黑树,提高查询效率。源码如下:

/*
 * 当添加一对键值对的时候,首先会去判断保存这些键值对的数组是不是初始化了,
 * 如果没有的话就初始化数组
 *  然后通过计算hash值来确定放在数组的哪个位置
 * 如果这个位置为空则直接添加,如果不为空的话,则取出这个节点来
 * 如果取出来的节点的hash值是MOVED(-1)的话,则表示当前正在对这个数组进行扩容,复制到新的数组,则当前线程也去帮助复制
 * 最后一种情况就是,如果这个节点,不为空,也不在扩容,则通过synchronized来加锁,进行添加操作
 *    然后判断当前取出的节点位置存放的是链表还是树
 *    如果是链表的话,则遍历整个链表,直到取出来的节点的key来个要放的key进行比较,如果key相等,并且key的hash值也相等的话,
 *          则说明是同一个key,则覆盖掉value,否则的话则添加到链表的末尾
 *    如果是树的话,则调用putTreeVal方法把这个元素添加到树中去
 *  最后在添加完成之后,会判断在该节点处共有多少个节点(注意是添加前的个数),如果达到8个以上了的话,
 *  则调用treeifyBin方法来尝试将处的链表转为树,或者扩容数组
 */
final V putVal(K key, V value, boolean onlyIfAbsent) {
	if (key == null || value == null) throw new NullPointerException();//K,V都不能为空,否则的话跑出异常
	int hash = spread(key.hashCode());    //取得key的hash值
	int binCount = 0;    //用来计算在这个节点总共有多少个元素,用来控制扩容或者转移为树
	for (Node[] tab = table;;) {    //
		Node f; int n, i, fh;
		if (tab == null || (n = tab.length) == 0)    
			tab = initTable();    //第一次put的时候table没有初始化,则初始化table
		else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {    //通过哈希计算出一个表中的位置因为n是数组的长度,所以(n-1)&hash肯定不会出现数组越界
			if (casTabAt(tab, i, null,        //如果这个位置没有元素的话,则通过cas的方式尝试添加,注意这个时候是没有加锁的
						 new Node(hash, key, value, null)))        //创建一个Node添加到数组中区,null表示的是下一个节点为空
				break;                   // no lock when adding to empty bin
		}
		/*
		 * 如果检测到某个节点的hash值是MOVED,则表示正在进行数组扩张的数据复制阶段,
		 * 则当前线程也会参与去复制,通过允许多线程复制的功能,一次来减少数组的复制所带来的性能损失
		 */
		else if ((fh = f.hash) == MOVED)    
			tab = helpTransfer(tab, f);
		else {
			/*
			 * 如果在这个位置有元素的话,就采用synchronized的方式加锁,
			 *     如果是链表的话(hash大于0),就对这个链表的所有元素进行遍历,
			 *         如果找到了key和key的hash值都一样的节点,则把它的值替换到
			 *         如果没找到的话,则添加在链表的最后面
			 *  否则,是树的话,则调用putTreeVal方法添加到树中去
			 *  
			 *  在添加完之后,会对该节点上关联的的数目进行判断,
			 *  如果在8个以上的话,则会调用treeifyBin方法,来尝试转化为树,或者是扩容
			 */
			V oldVal = null;
			synchronized (f) {
				if (tabAt(tab, i) == f) {        //再次取出要存储的位置的元素,跟前面取出来的比较
					if (fh >= 0) {                //取出来的元素的hash值大于0,当转换为树之后,hash值为-2
						binCount = 1;            
						for (Node e = f;; ++binCount) {    //遍历这个链表
							K ek;
							if (e.hash == hash &&        //要存的元素的hash,key跟要存储的位置的节点的相同的时候,替换掉该节点的value即可
								((ek = e.key) == key ||
								 (ek != null && key.equals(ek)))) {
								oldVal = e.val;
								if (!onlyIfAbsent)        //当使用putIfAbsent的时候,只有在这个key没有设置值得时候才设置
									e.val = value;
								break;
							}
							Node pred = e;
							if ((e = e.next) == null) {    //如果不是同样的hash,同样的key的时候,则判断该节点的下一个节点是否为空,
								pred.next = new Node(hash, key,        //为空的话把这个要加入的节点设置为当前节点的下一个节点
														  value, null);
								break;
							}
						}
					}
					else if (f instanceof TreeBin) {    //表示已经转化成红黑树类型了
						Node p;
						binCount = 2;
						if ((p = ((TreeBin)f).putTreeVal(hash, key,    //调用putTreeVal方法,将该元素添加到树中去
													   value)) != null) {
							oldVal = p.val;
							if (!onlyIfAbsent)
								p.val = value;
						}
					}
				}
			}
			if (binCount != 0) {
				if (binCount >= TREEIFY_THRESHOLD)    //当在同一个节点的数目达到8个的时候,则扩张数组或将给节点的数据转为tree
					treeifyBin(tab, i);    
				if (oldVal != null)
					return oldVal;
				break;
			}
		}
	}
	addCount(1L, binCount);    //计数
	return null;
}

Synchronized 是靠对象的对象头和此对象对应的 monitor来保证上锁的,也就是对象头里的重量级锁标志指向了monitor,而monitor呢,内部则保存了一个当前线程,也就是抢到了锁的线程。

那么这里的这个f是什么呢?它是 Node链表里的每一个Node,也就是说,Synchronized是将每一个 Node对象作为了一个锁,这样做的好处是将锁细化了。也就是说,除非两个线程同时操作一个Node,注意是一个 Node而不是一个Node链表哦,那么才会争抢同一把锁。

如果使用 ReentrantLock其实也可以将锁细化成这样的,只要让 Node类继承 ReentrantLock就行了,这样的话调用f.lock()就能做到和Synchronized(f)同样的效果,但为什么不这样做呢?

请大家试想一下,锁已经被细化到这种程度了,那么出现并发争抢的可能性还高吗?还有就是,哪怕出现争抢了,只要线程可以在30到50次自旋里拿到锁,那么Synchronized就不会升级为重量级锁,而等待的线程也就不用被挂起,我们也就少了挂起和唤醒这个上下文切换的过程开销。

但如果是 ReentrantLock呢?它则只有在线程没有抢到锁,然后新建 Node节点后再尝试一次而已,不会自旋,而是直接被挂起,这样一来,我们就很容易会多出线程上下文开销的代价。当然,你也可以使用tryLock(),但是这样又出现了一个问题,你怎么知道 tryLock的时间呢?在时间范围里还好,假如超过了呢?

所以,在锁被细化到如此程度上,使用 Synchronized是最好的选择了。这里再补充一句,Synchronized  和ReentrantLock他们的开销差距是在释放锁时唤醒线程的数量,Synchronized是唤醒锁池里所有的线程+刚好来访问的线程,而 ReentrantLock则是当前线程后进来的第一个线程+刚好来访问的线程。

如果是线程并发量不大的情况下,那么 Synchronized因为自旋锁、偏向锁、轻量级锁的原因,不用将等待线程挂起,偏向锁甚至不用自旋,所以在这种情况下要比 ReentrantLock高效。

八、有序的 Map实现类有哪些,他们是怎么保证有序的。


TreeMap 和 LinkedHashMap是有序的(TreeMap默认升序,LinkedHashMap则记录了插入顺序)。
【1】ThreeMap 是如何保证其迭代输出是有序的呢?其实从宏观上来讲,就相当于树的中序遍历(LDR[左-根-右])。
【2】LinkedHashMap采用的 hash算法和 HashMap相同,但是它重新定义了数组中保存的元素Entry,该Entry除了保存当前对象的引用外,还保存了其上一个元素 before和下一个元素 after的引用,从而在哈希表的基础上又构成了双向链接列表。

九、抽象类和接口的区别,类可以继承多个类么,接口可以继承多个接口么,类可以实现多个接口么


【1】抽象类和接口都不能直接实例化,如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。
【2】抽象类要被子类继承,接口要被类实现。
【3】接口只能做方法声明,抽象类中可以做方法声明,也可以做方法实现。
【4】接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。
【5】抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。同样,一个实现接口的时候,如不能全部实现接口方法,那么该类也只能为抽象类。
【6】抽象方法只能声明,不能实现。abstract void abc();不能写成abstract void abc(){}。
【7】抽象类里可以没有抽象方法,但如果一个类里有抽象方法,那么这个类只能是抽象类。
【8】抽象方法要被实现,所以不能是静态的,也不能是私有的。
【9】接口可继承接口,并可多个继承接口,但类只能单个继承。

十、继承和聚合的区别在哪


【1】继承:指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系;在 Java中此类关系通过关键字 extends明确标识,在设计时一般没有争议性;
【2】聚合:是关联关系的一种特例,他体现的是整体与部分、拥有的关系,即 has-a的关系,此时整体与部分之间是可分离的,他们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享;比如计算机与CPU、公司与员工的关系等;表现在代码层面,和关联关系是一致的,只能从语义级别来区分;

十一、IO模型有哪些,讲讲你理解的 NIO ,他和 BIO,AIO的区别是啥,谈谈 Reactor模型。


之前看过一篇NIO与IO:https://blog.csdn.net/zhengzhaoyang122/article/details/81410115
之前看过一篇Reactor:https://blog.csdn.net/zhengzhaoyang122/article/details/101391385
Reactor:一般情况下,I/O 复用机制需要事件分发器(event dispatcher)。事件分发器:即将那些读写事件源分发给各读写事件的处理者。开发人员在开始的时候需要在分发器那里注册感兴趣的事件,并提供相应的处理者(event handler),或者是回调函数;事件分发器在适当的时候,会将请求的事件分发给这些 handler或者回调函数。
涉及到事件分发器的两种模式称为:Reactor(NIO通常采用Reactor模式)和Proactor(AIO通常采用Proactor模式)。 Reactor模式是基于同步I/O的,而 Proactor模式是和异步I/O相关的。在 Reactor模式中,事件分发器等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是 socket可读写),事件分发器就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。
在 Reactor中实现读:1)、注册读就绪事件和相应的事件处理器。2)、事件分离器等待事件。3)、事件到来,激活分离器,分离器调用事件对应的处理器。4)、事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。

十二、反射的原理,反射创建类实例的三种方式是什么


 ☞ 反射原理:Java语言编译之后会生成一个.class文件,反射就是通过字节码文件找到某一个类、类中的方法以及属性等。
  1)、获取类对象:通过类名获取 Class对象,Class c = Class.forName("类的完全路径");通过 Class对象获取具体的类对象:Object o = (Object) c.newInstance();
  2)、获取类中的构造方法:getConstructor()等方法;
  3)、获取类中的属性:getFields()等方法;
  4)、获取类中的方法:getMethods()等方法;
 ☞ 创建类实例的三种方式 :1)、调用类的 Class对象的newInstance方法,该方法会调用对象的默认构造器,如果没有默认构造器,会调用失败。

Class classType = ExtendType.class;
Object inst = classType.newInstance();
System.out.println(inst);
 
//输出:
/*Type:Default Constructor
 *ExtendType:Default Constructor
 *com.quincy.ExtendType@d80be3
 */

2)、调用默认Constructor 对象的 newInstance方法:

Class classType = ExtendType.class;
Constructor constructor1 = classType.getConstructor();
Object inst = constructor1.newInstance();
System.out.println(inst);
 
//输出:
/*Type:Default Constructor
 *ExtendType:Default Constructor
 *com.quincy.ExtendType@1006d75
 */


  3)、调用带参数 Constructor对象的 newInstance方法:

Constructor constructor2 =classType.getDeclaredConstructor(int.class, String.class);
Object inst = constructor2.newInstance(1, "123");
System.out.println(inst);
 
//输出:
/*Type:Default Constructor
 *ExtendType:Constructor with parameters
 *com.quincy.ExtendType@15e83f9
 */


十三、反射中,Class.forName和 ClassLoader区别


【1】Class.forName(className)方法,内部实际调用的方法是 Class.forName(className,true,classloader);第2个 boolean参数表示类是否需要初始化,Class.forName(className)默认是需要初始化。一旦初始化,就会触发目标对象的 static 块代码执行,static 参数也会被再次初始化。
【2】ClassLoader.loadClass(className)方法,内部实际调用的方法是 ClassLoader.loadClass(className,false);
第2个boolean参数,表示目标对象是否进行链接,false表示不进行链接,由上面介绍可知,不进行链接意味着不进行包括初始化等一些列步骤,那么静态块和静态对象就不会得到执行。
classload 与 forName验证】:

//1、需要加载的目标类
public class Line {
    static {
        System.out.println("静态代码块执行: loading line");
    }
}
 
//2、测试类
package com.reflect;
 
public class ClassloaderAndForNameTest {
    public static void main(String[] args) {
        String wholeNameLine = "com.reflect.Line";
        System.out.println("下面是测试Classloader的效果");
        testClassloader(wholeNameLine);
        System.out.println("----------------------------------");
        System.out.println("下面是测试Class.forName的效果");
        testForName(wholeNameLine);
    }
 
    /**
     * classloader
     * @param wholeNameLine
     */
    private static void testClassloader(String wholeNameLine) {
        Class line;
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        try {
            line = loader.loadClass(wholeNameLine);
            System.out.println("line " + line.getName());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
 
    /**
     * Class.forName
     * @param wholeNameLine
     */
    private static void testForName(String wholeNameLine) {
        try {
            Class line = Class.forName(wholeNameLine);
            System.out.println("line   " + line.getName());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}


输出结果】:根据运行结果,可以看到,classloader并没有执行静态代码块,如开头的理论所说。而下面的 Class.forName加载完之后,就里面执行了静态代码块,可以看到 line 静态代码块执行结果是一起的,然后才是各自的打印结果。也说明上面理论是OK的。

下面是测试Classloader的效果
line com.reflect.Line
----------------------------------
下面是测试Class.forName的效果
静态代码块执行: loading line
line   com.reflect.Line

应用场景】:在我们熟悉的 Spring框架中的 IOC的实现就是使用的 ClassLoader。而在我们使用 JDBC时通常是使用Class.forName()方法来加载数据库连接驱动。这是因为在JDBC规范中明确要求Driver(数据库驱动)类必须向 DriverManager注册自己。

十四、描述动态代理的几种实现方式,分别说出相应的优缺点

【1】JDK动态代理:底层封装了实现细节,格式固定,代码简单。直接调用 java.lang.reflect.Proxy静态方法 newProxyInstance即可;JDK底层是利用反射机制,需要基于接口方式,这是由于被代理的对象必须是一个类,且必须有父接口;被代理的类需要增强的方法必须在父接口中出现;

/* 1、ClassLoader loader,:指定当前目标对象使用类加载器,获取加载器的方法是固定的
 * 2、Class[] interfaces,:目标对象实现的接口的类型,使用泛型方式确认类型
 * 3、InvocationHandler h:事件处理,执行目标对象的方法时,会触发事件处理器的方法,会把当前执行目标对象的方法作为参数传入
 */
static Object newProxyInstance(ClassLoader loader, Class[] interfaces,InvocationHandler h )


static Object newProxyInstance(ClassLoader loader, Class[] interfaces,InvocationHandler h )
【2】CGlib动态代理:也叫作子类代理:则是基于ASM框架,实现了无反射机制进行代理,利用空间来换取了时间,代理效率高于JDK。它是在内存中构建一个子类对象从而实现对目标对象功能的扩展,广泛的被许多AOP的框架使用。
举个栗子:被代理的类:

public class TargetProxy {
	public void save() {
		System.out.println("假如这个是Spring的service层的方法");
	}
}

 代理对象:

public class ProxyFactory implements MethodInterceptor {
	private Object target;
 
	public ProxyFactory(Object target) {
		this.target = target;
	}
 
	// 给目标对象创建一个代理对象
	public Object getProxyInstance() {
		Enhancer en = new Enhancer();
		en.setSuperclass(target.getClass());
                // 回调方法
		en.setCallback(this);
                // 创建代理对象
		return en.create();
	}
 
	@Override
       /** 
        * 调用方法 
        */  
	public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
		System.out.println("开始事务...");
                //执行方法
		Object returnValue = method.invoke(target, args);
		System.out.println("提交事务...");
		return returnValue;
	}
}


  执行测试 :

public class CglibProxy {
	public static void main(String[] args) {
		// 目标对象
		TargetProxy target = new TargetProxy();
		ProxyFactory factory = new ProxyFactory(target);
		target = (TargetProxy) factory.getProxyInstance();
		// 执行代理对象的方法
		target.save();
	}
}


【博客连接】:https://blog.csdn.net/zhengzhaoyang122/article/details/99168440

十五、动态代理与CGlib实现的区别


动态代理的目标对象一定要实现接口,否则不能用动态代理。但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候就可以使用 CGlib以目标对象子类的方式类实现代理。

十六、为什么 CGlib方式可以对接口实现代理


因为 Cglib是一个强大的高性能的代码生成包,它可以在运行期扩展 java类与实现 java接口。

十七、final 的用途


final 在 Java中是一个保留的关键字,可以声明 成员变量、方法、类以及本地变量。一旦你将引用声明作 final,你将不能改变这个引用了,编译器会检查代码,如果你试图将变量再次初始化的话,编译器会报编译错误。
【1】final变量:凡是对成员变量或者本地变量(在方法中的或者代码块中的变量称为本地变量)声明为 final的都叫作 final变量。final变量经常和 static关键字一起使用,作为常量。
【2】final方法:方法前面加上 final关键字,代表这个方法不可以被子类的方法重写。final方法比非 final方法要快,因为在编译的时候已经静态绑定了,不需要在运行时再动态绑定。
【3】final类:final 修饰的类通常功能是完整的,它不能被继承。Java 中有许多类是 final的,譬如String,Interger以及其他包装类。

十八、写出三种单例模式实现


【1】懒汉式单例模式,线程不安全。

//这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。
public class Singleton {
    private static Singleton instance;
    private Singleton (){}
 
    public static synchronized Singleton getInstance() {
	if (instance == null) {
	    instance = new Singleton();
	}
	return instance;
    }
}


【2】饿汉式单例模式

/*
 *这种方式基于classloder机制避免了多线程的同步问题,instance在类装载时就实例化。目前java单例是指一
 *个虚拟机的范围,因为装载类的功能是虚拟机的,所以一个虚拟机在通过自己的ClassLoader装载饿汉式实现单
 *例类的时候就会创建一个类的实例。这就意味着一个虚拟机里面有很多ClassLoader,而这些classloader都能
 *装载某个类的话,就算这个类是单例,也能产生很多实例。当然如果一台机器上有很多虚拟机,那么每个虚拟机
 *中都有至少一个这个类的实例的话,那这样 就更不会是单例了。(这里讨论的单例不适合集群!) 
 */
public class Singleton {  
    private static Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
    return instance;  
    }  
}


【3】静态内部类

/*这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,这种方式是Singleton类被
 *装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,
 *才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载!这个时候,这种方式相比第2种方式就显得很合理。
 */
public class Singleton {  
    private static class SingletonHolder {  
    private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
    return SingletonHolder.INSTANCE;  
    }  
} 


【4】枚举方式

/*这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反
 *序列化重新创建新的对象,可谓是很坚强的壁垒啊,不过,个人认为由于1.5中才加入enum特性,用这种方式写
 *不免让人感觉生疏,在实际工作中,我也很少看见有人这么写过。
 */
public enum Singleton {
    INSTANCE;
    public void whateverMethod() {
    }
}


【5】双重校验锁(jdk1.5)

/*这样方式实现线程安全地创建实例,而又不会对性能造成太大影响。它只是第一次创建实例的时候同步,以后就不需要同步了。
 *由于volatile关键字屏蔽了虚拟机中一些必要的代码优化,所以运行效率并不是很高,因此建议没有特别的需要不要使用。双重检验锁方式的单例不建议大量使用,根据情况决定。 
 */
public class Singleton {
    private volatile static Singleton singleton;
    private Singleton (){}
    public static Singleton getSingleton() {
	if (singleton == null) {
	    synchronized (Singleton.class) {
		if (singleton == null) {
		    singleton = new Singleton();
		}
	    }
	}
	return singleton;
    }
}


【博客链接】:https://blog.csdn.net/zhengzhaoyang122/article/details/79860091

十九、如何在父类中为子类自动完成所有的 hashcode 和 equals实现?这么做有何优劣


父类已经覆盖了equals,从父类继承过来的行为对于子类也是合适的。大多数的 Set实现都从 AbstractSet继承 equals实现,List实现从 AbstractList 继承 equals实现,Map 实现从 AbstractMap继承 equals实现。

二十、请结合OO设计理念,谈谈访问修饰符public、private、protected、default在应用设计中的作用


  同一个类 同一个包 不同包的子类 不同包的非子类
Private      
Default    
Protected  
Public


【1】Public: Java 语言中访问限制最宽的修饰符,一般称之为“公共的”。被其修饰的类、属性以及方法不仅可以跨类访问,而且允许跨包(package)访问。 
【2】Private: Java语言中对访问权限限制的最窄的修饰符,一般称之为“私有的”。被其修饰的类、属性以及方法只能被该类的对象访问,其子类不能访问,更不能允许跨包访问。
【3】Protect:介于public 和 private 之间的一种访问修饰符,一般称之为“保护形”。被其修饰的类、属性以及方法只能被类本身的方法及子类访问,即使子类在不同的包中也可以访问。
【4】default:即不加任何访问修饰符,通常称为“默认访问模式“。该模式下,只允许在同一个包中进行访问。

二十一、深拷贝和浅拷贝区别


【1】 浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间。
【2】深拷贝(.clone())不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。

二十二、数组和链表数据结构描述,各自的时间复杂度


【1】数组:是将元素在内存中连续存放,由于每个元素占用内存相同,可以通过下标迅速访问数组中任何元素。但是如果要在数组中增加一个元素,需要移动大量元素,在内存中空出一个元素的空间,然后将要增加的元素放在其中。同样的道理,如果想删除一个元素,同样需要移动大量元素去填掉被移动的元素。如果应用需要快速访问数据,很少插入和删除元素,就应该用数组。数组从栈中分配空间,对于程序员方便快速,但自由度小。数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况。
【2】链表:中的元素在内存中不是顺序存储的,而是通过存在元素中的指针联系到一起,每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针。如果要访问链表中一个元素,需要从第一个元素开始,一直找到需要的元素位置。但是增加和删除一个元素对于链表数据结构就非常简单了,只要修改元素中的指针就可以了。如果应用需要经常插入和删除元素你就需要用链表。链表从堆中分配空间,自由度大但申请管理比较麻烦。链表动态地进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。
【3】时间复杂度:数组利用下标定位,时间复杂度为O(1),链表定位元素时间复杂度O(n);数组插入或删除元素的时间复杂度O(n),链表的时间复杂度O(1)。

二十三、Error 和 Exception的区别,CheckedException,RuntimeException的区别


       Java面试---JAVA基础_第1张图片
【1】Error:当程序发生不可控的错误时,通常做法是通知用户并中止程序的执行。与异常不同的是 Error及其子类的对象不应被抛出。Error 是 Throwable的子类,代表编译时间和系统错误,用于指示合理的应用程序不应该试图捕获的严重问题。Error由Java虚拟机生成并抛出,包括动态链接失败,虚拟机错误等。程序对其不做处理。
【2】Exception一般分为 Checked异常和 Runtime异常,所有 RuntimeException类及其子类的实例被称为 Runtime异常,不属于该范畴的异常则被称为 CheckedException。
1)、Checked 异常:只有 java语言提供了Checked异常,Java 认为Checked异常都是可以被处理的异常,所以 Java程序必须显示处理 Checked异常。如果程序没有处理 Checked异常,该程序在编译时就会发生错误无法编译。这体现了 Java的设计哲学:没有完善错误处理的代码根本没有机会被执行。对 Checked异常处理方法有两种:①、当前方法知道如何处理该异常,则用try-catch块来处理该异常。②、当前方法不知道如何处理,则在定义该方法是声明抛出该异常。我们比较熟悉的Checked异常有:
 ● Java.lang.ClassNotFoundException
 ● Java.lang.NoSuchMetodException
 ● Java.io.IOException
2)、RuntimeException:Runtime 如除数是0和数组下标越界等,其产生频繁,处理麻烦,若显示申明或者捕获将会对程序的可读性和运行效率影响很大。所以由系统自动检测并将它们交给缺省的异常处理程序。当然如果你有处理要求也可以显示捕获它们。我们比较熟悉的 RumtimeException类的子类有:
 ● Java.lang.ArithmeticException
 ● Java.lang.ArrayStoreExcetpion
 ● Java.lang.ClassCastException
 ● Java.lang.IndexOutOfBoundsException
 ● Java.lang.NullPointerException

二十四、请列出5个运行时异常。


   ● ClassCastException (类转换异常)
   ● IllegalArgumentException (非法参数异常)
   ● IndexOutOfBoundsException (下标越界异常)
   ● NullPointerException (空指针异常)
   ● ArithmeticException  (算术运算异常)
   ● OutOfMemoryError  (内存不足)
   ● StackOverflowError  (堆栈溢出)
   ● ClassNotFoundException  (找不到类异常)
   ● InterruptedException (终止异常)

二十五、在自己的代码中,如果创建一个 java.lang.String类,这个类是否可以被类加载器加载?为什么


不会被加载。原因:类加载器的委托机制。类的加载过程采用父亲委托机制。这种机制能更好的保证 java平台的安全。在此委托机制中,除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。当 Java程序请求加载器loader加载Sample类时,loader首先委托自己的父加载器去加载Sample类,若父加载器能加载,则由父加载器完成加载任务,否则才由加载器 loader本身加载 Sample类。

Java面试---JAVA基础_第2张图片

加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是说当发现这个类没有的时候会先去让自己的父类去加载。那么例子中我们自己写的String应该是被Bootstrap ClassLoader加载了,所以App ClassLoader就不会再去加载我们写的String类了,导致我们写的String类是没有被加载的。 

二十六、说一说你对 java.lang.Object对象中 hashCode和 equals方法的理解。在什么场景下需要重新实现这两个方法


【1】hashCode:hashCode是 jdk根据对象的地址或者字符串或者数字算出来的 int类型的数值,也就是哈希码,哈希码并不是完全唯一的,它是一种算法,让同一个类的对象按照自己不同的特征尽量的有不同的哈希码,但不表示不同的对象哈希码完全不同。
【2】equals:在 Object这个类里面提供的 Equals()方法默认的实现是比较当前对象的引用和你要比较的那个引用它们指向的是否是同一个对象。
【3】当我们需要对值进行比较时,就需要重写这两个方法。例如 String就重写了这两个方法,比较的是 value值是否相等。

二十七、在 jdk1.5中,引入了泛型,泛型的存在是用来解决什么问题


泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数,能够解决代码复用的问题。常见的一种情况是,你有一个函数,它带有一个参数,参数类型是A,然而当参数类型改变成 B的时候,你不得不复制这个函数。除此之外泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,消除显示的类型强制转换,以提高代码的重用率。使用方法:

public class Stack{
        private T[] m_item;
        public T Pop(){...}
        public void Push(T item){...}
        public Stack(int i)
        {
            this.m_item = new T[i];
        }
}

  类的写法不变,只是引入了通用数据类型T就可以适用于任何数据类型,并且类型安全的。这个类的调用方法:

//实例化只能保存int类型的类
Stack a = new Stack(100);
      a.Push(10);
      a.Push("8888"); //这一行编译不通过,因为类a只接收int类型的数据
      int x = a.Pop();
 
//实例化只能保存string类型的类
Stack b = new Stack(100);
      b.Push(10);    //这一行编译不通过,因为类b只接收string类型的数据
      b.Push("8888");
      string y = b.Pop();


二十八、这样的 a.hashcode() 有什么用,与 a.equals(b)有什么关系


hashcode()方法提供了对象的 hashCode值,是一个 native方法,返回的默认值与 System.identityHashCode(obj)一致。通常这个值是对象头部的一部分二进制位组成的数字,具有一定的标识对象的意义存在,但绝不定于地址。
作用是:用一个数字来标识对象。比如在 HashMap、HashSet 等类似的集合类中,如果用某个对象本身作为Key,即要基于这个对象实现 Hash的写入和查找,那么对象本身如何实现这个呢?就是基于 hashcode这样一个数字来完成的,只有数字才能完成计算和对比操作。
equals 与 hashcode的关系:equals 相等的两个对象,则重写后的 hashcode一定要相等。但是 hashcode相等的两个对象 equals 不一定相等。

二十九、有没有可能2个不相等的对象有相同的 hashcode


hashCode 是所有 java对象的固有方法,如果不重载的话,返回的实际上是该对象在 jvm的堆上的内存地址,而不同对象的内存地址肯定不同,所以这个 hashCode也就肯定不同了。如果重载了的话,由于采用的算法的问题,有可能导致两个不同对象的hashCode相同。
hashcode()方法返回一个 int数,在 Object类中的默认实现是“将该对象的内部地址转换成一个整数返回”。接下来有两个个关于这两个方法的重要规范(我只是抽取了最重要的两个,其实不止两个):
【1】若重写 equals(Object obj)方法,有必要重写 hashcode()方法,确保通过 equals(Object obj)方法判断结果为true的两个对象具备相等的 hashcode()返回值。说得简单点就是:“如果两个对象相同,那么他们的 hashcode应该相等”。不过请注意:这个只是规范,如果你非要写一个类让 equals(Object obj) 返回 true而 hashcode()返回两个不相等的值,编译和运行都是不会报错的。不过这样违反了Java规范,程序也就埋下了BUG。
【2】如果 equals(Object obj)返回 false,即两个对象“不相同”,并不要求对这两个对象调用 hashcode()方法得到两个不相同的数。说的简单点就是:“如果两个对象不相同,他们的 hashcode可能相同”。

三十、Java 中的 HashSet 内部是如何工作的


它是基于 HashMap 实现的,底层采用 HashMap 来保存元素。所有 Set接口的类内部都是由 Map做支撑的。HashSet 用HashMap 对它的内部对象进行排序。你一定好奇输入一个值到 HashMap,我们需要的是一个键值对,但是我们传给 HashSet的是一个值。实际上我们插入到 HashSet中的值在 Map对象中起的是键的作用,因为它的值Java用了一个常量。所以在键值对中所有的键的值都是一样的。

class Test
{
    public static void main(String[]args)
    {
        HashSet h = new HashSet();
 
        // adding into HashSet
        h.add("India");
        h.add("Australia");
        h.add("South Africa");
        h.add("India");// adding duplicate elements
 
        // printing HashSet
        System.out.println(h);//[Australia, South Africa, India]
        System.out.println("List contains India or not:" +
                           h.contains("India"));//true
 
        // Removing an item
        h.remove("Australia");
        System.out.println("List after removing Australia:"+h);//[South Africa, India]
 
        // Iterating over hash set items
        System.out.println("Iterating over list:");
        Iterator i = h.iterator();
        while (i.hasNext())
            System.out.println(i.next());//South Africa  India
    }
}


三十、什么是序列化,怎么序列化,为什么序列化,反序列化会遇到什么问题,如何解决


序列化 (Serialization):将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
序列化实现:1)、Serializable 接口:类通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。 2)、Externalizable接口:除了Serializable 之外,java 中还提供了另一个序列化接口 Externalizable。Externalizable继承了Serializable,该接口中定义了两个抽象方法:writeExternal()与readExternal()。当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写 writeExternal()与 readExternal()方法。3)、ObjectOutput和ObjectInput 接口。4)、ObjectOutputStream类和ObjectInputStream类。
注意:1)、Transient关键字:作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。2)、序列化ID:虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)。序列化 ID 在 Eclipse 下提供了两种生成策略,一个是固定的 1L,一个是随机生成一个不重复的 long 类型数据(实际上是使用 JDK 工具生成),在这里有一个建议,如果没有特殊需求,就是用默认的 1L 就可以,这样可以确保代码一致时反序列化成功。那么随机生成的序列化 ID 有什么作用呢,有些时候,通过改变序列化 ID 可以用来限制某些用户的使用。
为什么序列化:1)、当你想把的内存中的对象保存到一个文件中或者数据库中时候;2)、当你想用套接字在网络上传送对象的时候; 3)、当你想通过RMI传输对象的时候;
反序列化问题及解决办法:1)、类里面一定要 serialVersionUID,否则旧数据会反序列化会失败。
解决方法:serialVersionUID 是根据该类名、方法名等数据生产的一个整数,用来验证版本是否一致。如果不加这个字段,当你的类修改了字段,在反序列化的时候会直接报异常:InvalidCastException,导致无法完成反序列化。
2)、一旦序列化保存到磁盘操作后,就不要修改类名了,否则旧数据会反序列化会失败。
解决方法:所以尽量把对象转换成 JSON保存更稳妥。

三十二、java8 的新特性


【1】Lambda 表达式:将函数式编程引入了Java。Lambda 允许把函数作为一个方法的参数,或者把代码看成数据。

String[] atp = {"Nadal", "Djokovic",  "Wawrinka"};  
List players =  Arrays.asList(atp);  
  
// 以前的循环方式  
for (String player : players) {  
     System.out.print(player + "; ");  
}  
// 使用 lambda 表达式以及函数操作(functional operation)  
players.forEach((player) -> System.out.print(player + "; "));  


【2】接口的默认方法与静态方法:我们可以在接口中定义默认方法,使用 default关键字,并提供默认的实现。所有实现这个接口的类都会接受默认方法的实现,除非子类提供的自己的实现。

public interface DefaultFunctionInterface {
    default String defaultFunction() {
        return "default function";
    }
}
 
//我们还可以在接口中定义静态方法,使用static关键字,也可以提供实现。
public interface StaticFunctionInterface {
    static String staticFunction() {
        return "static function";
    }
}


【3】方法引用:通常与 Lambda表达式联合使用,可以直接引用已有 Java类或对象的方法。
【4】重复注解:在 Java 5中使用注解有一个限制,即相同的注解在同一位置只能声明一次。Java 8 引入重复注解,这样相同的注解在同一地方也可以声明多次。重复注解机制本身需要用@Repeatable注解。Java 8在编译器层做了优化,相同注解会以集合的方式保存,因此底层的原理并没有变化。
【5】扩展注解的支持:Java 8 扩展了注解的上下文,几乎可以为任何东西添加注解,包括局部变量、泛型类、父类与接口的实现,连方法的异常也能添加注解。
【6】Optional:Java 8 引入 Optional 类来防止空指针异常,Optional 类最先是由 Google 的 Guava项目引入的。Optional类实际上是个容器:它可以保存类型T的值,或者保存null。使用Optional类我们就不用显式进行空指针检查了。

Optional str = Optional.of("test");
//1、判断str是否为null,如果不为null,则为true,否则为false
if (str.isPresent()) {
	//get用于获取变量的值,当变量不存在的时候会抛出NoSuchElementException,如果不能确定变量一定存在值,则不推荐使用
	str.get();
}


【7】Stream:Stream API 是把真正的函数式编程风格引入到 Java中。其实简单来说可以把 Stream理解为 MapReduce,当然Google 的 MapReduce的灵感也是来自函数式编程。她其实是一连串支持连续、并行聚集操作的元素。从语法上看,也很像 Linux的管道、或者链式编程,代码写起来简洁明了,非常酷帅!

用List对象中的age当做key 其余当做value 放入map中。

代码:

public class StreamTest {
    public static void main(String[] args) {
        List list = new ArrayList();
        User user = new User();
        user.setAge(12);
        user.setName("张三");
        list.add(user);
        User user1 = new User();
        user1.setAge(14);
        user1.setName("李四");
        list.add(user1);
        User user2 = new User();
        user2.setAge(16);
        user2.setName("王五");
        list.add(user2);
        Map> collect = list.stream().collect(Collectors.groupingBy(User::getAge));
        System.out.println(collect);
    }
}

【8】Date/Time API (JSR 310):Java 8 新的 Date-Time API (JSR 310)受Joda-Time的影响,提供了新的 java.time包,可以用来替代 java.util.Date和 java.util.Calendar。一般会用到 Clock、LocaleDate、LocalTime、LocaleDateTime、ZonedDateTime、Duration这些类,对于时间日期的改进还是非常不错的。
【9】JavaScript 引擎 Nashorn:Nashorn允许在 JVM上开发运行 JavaScript应用,允许 Java与 JavaScript相互调用。
【10】Base64:在 Java 8中,Base64 编码成为了Java类库的标准。Base64 类同时还提供了对URL、MIME友好的编码器与解码器。

三十三、sort() 底层使用的是什么算法


collections.sort 方法底层就是调用的 Arrays.sort() 方法,而 Array.sort 使用了两种排序方法,快速排序和优化的归并排序。快速排序主要是对那些基本数据类型(int,short、long等)排序,而归并排序用于对 Object 类型进行排序。使用不同类型的排序算法主要是由于快速排序是不稳定的,而归并排序是稳定的。这里的稳定是指当比较相等的数据时,会按照原有的数据顺序排列。对于基本数据类型,稳定性没有意义,而对于 Object .+类型,稳定性是比较重要的,因为对象相等的判断可能只是判断关键属性,最好保存相等对象的非关键属性的顺序与排序前一致;另外一个原因是由于归并排序相对而言比较次数比快速排序少,对象引用的移动次数比快速排序多,而对于对象而言,比较一般比移动耗时。此外,对于数组排序。快速排序的 sort() 采用递归实现,数组规模太大时会出现堆栈溢出,而归并排序 sort() 采用非递归排序,不存在此问题。
总结】:首先判断需要排序的数据量是否大于 60;
【1】小于60:使用插入排序,插入排序是稳定的;
【2】大于60:数据量会根据数据类型选择排序方式:基本类型使用快速排序。因为基本类型都是指向同一个常量池不需要考虑稳定性。Object 类型:使用归并排序。因为归并排序具有稳定性。
注意:不管是排序排序还是归并排序。在二分的时候小于60的数据量依旧会使用插入排序。

jdk1.8】:进入 Arrays.sort(s); 源代码:

static void sort(int[] a, int left, int right,
				 int[] work, int workBase, int workLen) {
	// 对小数组使用快速排序 QUICKSORT_THRESHOLD = 286
	if (right - left < QUICKSORT_THRESHOLD) {
		sort(a, left, right, true);
		return;
	}
	//......
}

数组一进来,会碰到第一个阀值QUICKSORT_THRESHOLD(286),注解上说,小过这个阀值的进入Quicksort (快速排序),其实并不全是,点进去 sort(a, left, right, true); 方法:

for (int i = left, j = i; i < right; j = ++i) {
	int ai = a[i + 1];
	while (ai < a[j]) {
		a[j + 1] = a[j];
		if (j-- == left) {
			break;
		}
	}
	a[j + 1] = ai;
}

【插入排序博客】: https://blog.csdn.net/zhengzhaoyang122/article/details/79896138

 至于大过 INSERTION_SORT_THRESHOLD(47)的,用一种快速排序的方法:
【1】从数列中挑出五个元素,称为 “基准”(pivot);
【2】重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
【3】递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

【快速排序博客】:https://blog.csdn.net/zhengzhaoyang122/article/details/102807398

这是少于阀值QUICKSORT_THRESHOLD(286)的两种情况,至于大于286的,它会进入归并排序(Merge Sort),但在此之前,它有个小动作:

// 检查数组是否接近排序
for (int k = left; k < right; run[count] = k) {
	if (a[k] < a[k + 1]) { // ascending
		while (++k <= right && a[k - 1] <= a[k]);
	} else if (a[k] > a[k + 1]) { // descending
		while (++k <= right && a[k - 1] >= a[k]);
		for (int lo = run[count] - 1, hi = k; ++lo < --hi; ) {
			int t = a[lo]; a[lo] = a[hi]; a[hi] = t;
		}
	} else { // equal
		for (int m = MAX_RUN_LENGTH; ++k <= right && a[k - 1] == a[k]; ) {
			if (--m == 0) {
				sort(a, left, right, true);
				return;
			}
		}
	}
	/*
	 * 阵列结构不高,使用快速排序而不是合并排序。
	 */
	if (++count == MAX_RUN_COUNT) {
		sort(a, left, right, true);
		return;
	}
}


这里主要作用是看他数组具不具备结构:实际逻辑是分组排序,每降序为一个组,像1,9,8,7,6,8。9到6是降序,为一个组,然后把降序的一组排成升序:1,6,7,8,9,8。然后最后的8后面继续往后面找。每遇到这样一个降序组,++count,当 count大于MAX_RUN_COUNT(67),被判断为这个数组不具备结构(也就是这数据时而升时而降),然后送给之前的 sort(里面的快速排序 )的方法(The array is not highly structured,use Quicksort instead of merge sort.)。如果 count 少于MAX_RUN_COUNT(67)的,说明这个数组还有点结构,就继续往下走下面的归并排序:

// 确定合并的替换基
byte odd = 0;
for (int n = 1; (n <<= 1) < count; odd ^= 1);
// 使用或创建用于合并的临时数组b
int[] b;                 // temp array; alternates with a
int ao, bo;              // array offsets from 'left'
int blen = right - left; // space needed for b
if (work == null || workLen < blen || workBase + blen > work.length) {
	work = new int[blen];
	workBase = 0;
}
if (odd == 0) {
	System.arraycopy(a, left, work, workBase, blen);
	b = a;
	bo = 0;
	a = work;
	ao = workBase - left;
} else {
	b = work;
	ao = 0;
	bo = workBase - left;
}

从这里开始,正式进入归并排序(Merge Sort)!

// Merging
for (int last; count > 1; count = last) {
	for (int k = (last = 0) + 2; k <= count; k += 2) {
		int hi = run[k], mi = run[k - 1];
		for (int i = run[k - 2], p = i, q = mi; i < hi; ++i) {
			if (q >= hi || p < mi && a[p + ao] <= a[q + ao]) {
				b[i + bo] = a[p++ + ao];
			} else {
				b[i + bo] = a[q++ + ao];
			}
		}
		run[++last] = hi;
	}
	if ((count & 1) != 0) {
		for (int i = right, lo = run[count - 1]; --i >= lo;
			b[i + bo] = a[i + ao]
		);
		run[++last] = right;
	}
	int[] t = a; a = b; b = t;
	int o = ao; ao = bo; bo = o;
}


 【总结】:从上面分析,Arrays.sort 并不是单一的排序,而是插入排序,快速排序,归并排序三种排序的组合,为此我画了个流程图:

Java面试---JAVA基础_第3张图片
O(nlogn)只代表增长量级,同一个量级前面的常数也可以不一样,不同数量下面的实际运算时间也可以不一样。数量非常小的情况下(就像上面说到的,少于47的),插入排序等可能会比快速排序更快。所以数组少于47的会进入插入排序。

快排数据越无序越快(加入随机化后基本不会退化),平均常数最小,不需要额外空间,不稳定排序。归排速度稳定,常数比快排略大,需要额外空间,稳定排序。所以大于或等于47或少于286会进入快排,而在大于或等于286后,会有个小动作:“// Check if the array is nearly sorted”。这里第一个作用是先梳理一下数据方便后续的归并排序,第二个作用就是即便大于286,但在降序组太多的时候(被判断为没有结构的数据,The array is not highly structured,use Quicksort instead of merge sort.),要转回快速排序。

三十四、常见算法的复杂度是多少


Java面试---JAVA基础_第4张图片

三十五、int 与 Integer 之间的数据比较


Integer i1=59;
int i2=59;
Integer i3=Integer.valueOf(59);
Integer i4=new Integer(59);
System.out.println(i2==i1); // true
System.out.println(i3==i4); // false
System.out.println(i2==i4); // true
System.out.println(i1==i3); // true


调试可看到:i1,i3 的地址值是一样的,i4地址不同。是因为直接使用 Integer包装类进行赋值的话,会调用常量池中的对象,是不会产生新对象的。而用构造方法的话,就会新开辟一个堆空间。Integer 和 int 进行比较的话,会自动拆装箱,所以值是一样的。

Java面试---JAVA基础_第5张图片

【若值不在常量池 [-128,127] 之间】:调试可得,我们看到,i1,i3地址值也不一样了,为什么呢,这里我们说,当值超过[-128,127]范围时,就会申请在堆中 new一个对象。

Integer i1=128;
int i2=128;
Integer i3=Integer.valueOf(128);
Integer i4=new Integer(128);
System.out.println(i2==i1); //true
System.out.println(i3==i4); //false
System.out.println(i2==i4); //true
System.out.println(i1==i3); //false


总结】:① Integer 直接赋值时,若值在[-128,127] 之间则不会申请新对象,会调用常量池中的对象;
  ② 若超过范围,则申请 new一个对象;
  ③ 若采用构造方法赋值,则在堆上开辟新空间;
  ④ Integer和 int进行 ==比较时,由于会自动拆箱,将 Integer转为 int,则直接看值的大小就可以。

【源码展示】:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}


三十六、函数式编程与面向对象编程的区别


【1】函数式编程(functional programming):又称泛函编程,是一种编程范型,它将电脑运算视为数学上的函数计算,并且避免使用程序状态以及易变对象。函数编程语言最重要的基础是λ演算(lambda calculus)。而且λ演算的函数可以接受函数当作输入(引数)和输出(传出值)。

函数式编程的优点】:在函数式编程中,由于数据全部都是不可变的,所以没有并发编程的问题,是多线程安全的。可以有效降低程序运行中所产生的副作用,对于快速迭代的项目来说,函数式编程可以实现函数与函数之间的热切换而不用担心数据的问题,因为它是以函数作为最小单位的,只要函数与函数之间的关系正确即可保证结果的正确性。函数式编程的表达方式更加符合人类日常生活中的语法,代码可读性更强。实现同样的功能函数式编程所需要的代码比面向对象编程要少很多,代码更加简洁明晰。函数式编程广泛运用于科学研究中,因为在科研中对于代码的工程化要求比较低,写起来更加简单,所以使用函数式编程开发的速度比用面向对象要高很多,如果是对开发速度要求较高但是对运行资源要求较低同时对速度要求较低的场景下使用函数式会更加高效。

【函数式编程的缺点】:由于所有的数据都是不可变的,所以所有的变量在程序运行期间都是一直存在的,非常占用运行资源。同时由于函数式的先天性设计导致性能一直不够。虽然现代的函数式编程语言使用了很多技巧比如惰性计算等来优化运行速度,但是始终无法与面向对象的程序相比,当然面向对象程序的速度也不够快。函数式编程虽然已经诞生了很多年,但是至今为止在工程上想要大规模使用函数式编程仍然有很多待解决的问题,尤其是对于规模比较大的工程而言。如果对函数式编程的理解不够深刻就会导致跟面相对象一样晦涩难懂的局面。

【2】面向对象编程(Object-oriented programming):是种具有对象概念的程序编程范型,同时也是一种程序开发的方法。它可能包含数据、属性、代码与方法。对象则指的是类的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关连的数据。在面向对象程序编程里,计算机程序会被设计成彼此相关的对象。

对象与对象之间的关系是面向对象编程首要考虑的问题,而在函数式编程中,所有的数据都是不可变的,不同的函数之间通过数据流来交换信息,函数作为FP中的一等公民,享有跟数据一样的地位,可以作为参数传递给下一个函数,同时也可以作为返回值。

面向对象编程的优点】:面向对象程序设计可以看作一种在程序中包含各种独立而又互相调用的对象的思想,这与传统的思想刚好相反。传统的程序设计主张将程序看作一系列函数的集合,或者直接就是一系列对电脑下达的指令。面向对象程序设计中的每一个对象都应该能够接收数据、处理数据并将数据传达给其它对象,因此它们都可以被看作一个小型的“机器”,即对象。目前已经被证实的是,面向对象程序设计推广了程序的灵活性和可维护性,并且在大型项目设计中广为应用。此外,支持者声称面向对象程序设计要比以往的做法更加便于学习,因为它能够让人们更简单地设计并维护程序,使得程序更加便于分析、设计、理解。同时它也是易拓展的,由于继承、封装、多态的特性,自然设计出高内聚、低耦合的系统结构,使得系统更灵活、更容易扩展,而且成本较低。在面向对象编程的基础上发展出来的23种设计模式广泛应用于现今的软件工程中,极大方便了代码的书写与维护。建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

【面向对象编程的缺点】:面向对象编程以数据为核心,所以在多线程并发编程中,多个线程同时操作数据的时候可能会导致数据修改的不确定性。在现在的软件工程中,由于面向对象编程的滥用,导致了很多问题。首先就是为了写可重用的代码而产生了很多无用的代码,导致代码膨胀,同时很多人并没有完全理解面向对象思想,为了面向对象而面向对象,使得最终的代码晦涩难懂,给后期的维护带来了很大的问题。所以对于大项目的开发,使用面向对象会出现一些不适应的情况。面向对象虽然开发效率高但是代码的运行效率比起面向过程要低很多,这也限制了面向对象的使用场景不能包括那些对性能要求很苛刻的地方。

三十七、equals 和 ==区别, 重写 equals一定要重写 hashcode方法吗?为什么? hashcode方法有什么作用?


对于基本类型来说 ,==比较两个基本类型的值是否相等,对于引用类型来说,==比较的是内个引用类型的内存地址。
equals 说明】:equals 用来比较的是两个对象的内容是否相等,由于所有的类都是继承自 java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖,调用的仍然是 Object类中的方法,而 Object中的 equals方法返回的却是 ==的判断。     【重写 equals一般是要重写 hashcode方法的,首先 equals与 hashcode间的关系如下】:
  1)、如果两个对象相同(即用 equals比较返回 true),那么它们的 hashCode值一定要相同;
  2)、如果两个对象的 hashCode相同,它们并不一定相同(即用 equals比较返回 false) ;

比如说两个字符串的 hashcode相同,但是这两个字符串可以是不同的字符串,对象也是同理。

至于 hashcode有什么用】:为了提高程序的效率才实现了 hashcode方法,先进行 hashcode的比较,如果不同,那没就不必在进行 equals的比较了,这样就大大减少了 equals比较的次数,很大程度上提高了比较的效率,一个很好的例子就是在集合中的使用;

三十八、Java序列化,有ID和没ID会出现问题吗

序列化 ID 问题】:两个客户端 A 和 B 试图通过网络传递对象数据,A 端将对象 C 序列化为二进制数据再传给 B,B 反序列化得到 C。
问题】:C 对象的全类路径假设为 com.yintong.UserInfo,在 A 和 B 端都有这么一个类文件,功能代码完全一致。也都实现了 Serializable 接口,但是反序列化时总是提示不成功。
解决】:虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。清单 1 中,虽然两个类的功能代码完全一致,但是序列化 ID 不同,他们无法相互序列化和反序列化。

你可能感兴趣的:(java基础,java面试题集锦-1)