很细心!Java 超全面试题整理(非常值得一看的java面试题)

新闻资讯2024-08-09 22:28小乐

很细心!Java 超全面试题整理(非常值得一看的java面试题)

Java基础知识

Java 并发的特点是什么: 可以在其中执行许多语句,而不必一次执行所有语句面向对象: 基于类和面向对象的编程语言。独立:独立的编程语言,支持一次编写,随处运行,即编译后的代码可以在所有支持Java的平台上运行。 Java的特点Java的特点包括以下几点

简单,Java会让你的工作变得更加轻松,让你专注于主要业务逻辑,而不必担心指针、运算符重载、内存回收等与主要业务无关的功能。可移植性,Java是平台无关的,这意味着在一个平台上编写的任何应用程序都可以轻松移植到另一个平台上。安全性,编译后,所有代码都会转换为字节码,人类无法读取。它使得开发无病毒、防篡改的系统/应用程序成为可能。动态的,具有适应变化环境的能力,可以支持动态内存分配,从而减少内存浪费,提高应用程序性能。分布式,Java 提供了有助于创建分布式应用程序的功能。使用远程方法调用(RMI),程序可以通过网络调用另一个程序的方法并获取输出。您可以通过从Internet 上的任何计算机调用方法来访问文件。这是一项革命性的功能,对于当今的互联网来说非常重要。健壮性,Java具有强大的内存管理功能,可以在编译和运行时检查代码,这有助于消除错误。高性能,Java最黑暗的技术就是字节码编程。编译成Java代码的字节码可以很容易地转换成本地机器代码。使用JIT 编译器实现高性能。解释型,Java被编译成字节码,由Java运行环境解释。多线程,Java 支持多个执行线程(也称为轻量级进程),包括一组同步原语。这使得使用线程进行编程变得更加容易,并且Java通过监视器模型实现了线程安全。描述值传递和引用传递之间的区别。如果你想真正了解,可以参考这篇文章:https://www.zhihu.com/question/31203609

简单来说,就是

值传递是指在调用函数时将实际参数复制到函数中。这样,如果函数修改了传递的形参,则不会影响实参。

引用传递是指在调用函数时将对象的地址直接传递给函数。如果修改形参,实参的值就会受到影响。

==和equals 有什么区别?==是Java中的运算符。它有两种比较方法。

对于基本数据类型,==判断两边的值是否相等public class DoubleCompareAndEquals { Person person1=new Person(24,'boy');人person2=new Person(24,'女孩');整数c=10;私有无效doubleCompare(){ int a=10;整数b=10; System.out.println(a==b); System.out.println(a==c); System.out.println(person1.getId()==person2.getId());对于引用类型,==判断两边的引用是否相等,即两个对象是否指向同一个内存区域。 private void equals(){ System.out.println(person1.getName().equals(person2.getName()));}equals是Java中所有对象的父类,即Object类定义的方法。它只能比较对象,并且指示两个引用的值是否相等。所以记住,并不是说==比较引用是否相等,equals比较值,这个需要区分。

equals用于对象之间的比较,具有以下特点

自反性:对于任何非空引用x,x.equals(x) 应该返回true。对称性:对于任何非空引用x 和y,如果x.equals(y) 为true,则y.equals(x) 也为true。传递性:对于任何非空参考值,都有三个值:x、y和z。如果x.equals(y) 返回true 并且y.equals(z) 返回true,则x.equals(z) 也应该返回true。一致性:对于任何非空引用x 和y,如果x.equals(y) 相等,则它们必须始终相等。非空性:对于任何非空引用的值x,x.equals(null) 必须返回false。 String中的equals是如何重写的? String在Java中表示字符串。 String 类很特殊。整个类都被final修饰了。也就是说String不能被任何类继承。对String string 的任何修改所有方法都会创建一个新字符串。

equals方法是Object类定义的方法。 Object是所有类的父类,当然也包括String。字符串重写equals 方法。我们来看看它是如何重写的。

首先判断两个待比较字符串的引用是否相等。如果引用相等,则直接返回true。如果不相等,则继续下面的判断,然后判断比较的对象是否是String的实例。如果不是,直接返回false。如果是,则比较两个字符串的长度,看看它们是否相等。如果长度不想等待,则无需比较;如果长度相同,则将比较字符串中的每个字符是否相等。一旦有一个字符不相等,则直接返回false。下面是它的流程图

这是另一个提醒。您可能想知道什么时候

if (this==anObject) { return true;}这个判断语句怎么会返回true呢?因为都是字符串,字符串比较不都是堆空间吗?乍一看,似乎永远不会消失,但你忘记了String.intern() 方法。它所代表的概念有不同的JDK版本。不同的区别

在JDK1.7及以后版本中,调用intern方法是判断运行时常量池中是否存在指定的字符串。如果没有,则将该字符串添加到常量池中,并返回常量池中的对象。

验证流程如下

私有无效StringOverrideEquals(){ String s1='aaa';字符串s2='aa' + new String('a');字符串s3=new String('aaa'); System.out.println(s1.intern() .equals(s1)); System.out.println(s1.intern().equals(s2)); System.out.println(s3.intern().equals(s1));} 首先s1.intern.equals(s1) 无论如何这都会返回true,因为s1 字符串在创建时已经存在于常量池中。那么第二条语句返回false,因为s1返回常量池中的对象,s2返回堆中的对象。第三条语句s3.intern.equals(s1)返回true,因为虽然s3对象在堆中创建了一个对象,但是s3中的'aaa'返回的是常量池中的对象。为什么重写equals方法必须重写hashcode方法。 equals方法和hashCode都是Object中定义的方法,它们经常被一起重写。

equals方法是用来比较对象大小是否相等的方法,hashcode方法是用来确定每个对象的哈希值的方法。如果只重写equals方法而不重写hashcode方法,很可能两个不同的对象会有相同的hashcode,造成冲突。例如

字符串str1='调用'; String str2='重位置';两者的hashcode相等,但equals可能不相等。

我们看一下hashCode的官方定义

把它们加起来:

如果Java运行时对同一个对象调用hashCode方法,那么无论调用多少次,都应该返回相同的hashCode。但是,在不同的Java程序中,hashCode方法返回的值可能不一致。如果两个对象的equals 相等,那么hashCode 一定相同。如果两个对象的equals不相等,那么hashCode也可能相同,所以需要重写hashCode方法,因为你不知道hashCode的底层结构(反正我也不知道,有很多牛都会教),所以需要重写hashCode方法,为不同的对象生成不同的hashCode值,这样可以提高不同对象的访问速度。 HashCode通常是通过将地址转换为整数来实现的。 String s1=new String('abc') 在内存中创建一两个对象。 String s1 声明了一个String 类型的s1 变量,它不是一个对象。使用new关键字会在堆中创建一个对象,另一个对象是abc,会在常量池中创建,所以一共创建了两个对象;如果常量池中已经存在abc,则将创建一个对象。

详细内容请阅读作者的另一篇文章:String、StringBuffer、StringBuilde独特详解

为什么String 是不可变的? jdk源码中String是如何定义的?为什么要这样设计呢?首先,我们来了解一下什么是不可变对象。不可变对象意味着一旦创建,其内部状态就无法修改。这是什么意思?换句话说,不可变对象需要遵守以下原则:

不可变对象的内部属性都是最终的。不可变对象的内部属性都是私有的。不可变对象不能提供任何可以修改内部状态的方法,setter 方法也不能。不可变对象不能被继承和扩展。而不是问String 为什么它是不可变的。更好讲一下如何将String设计成不可变的。

String类是一个独立于Java基本数据类型而存在的对象。您可以将String 视为字符串的集合。 String被设计成final的,这意味着一旦创建了String对象,它的值就不能被修改,任何修改String值的方法都是重新创建一个字符串。 String对象创建后,会存在于运行时常量池中。运行时常量池是方法区的一部分。 JDK1.7之后被移至堆中。

不可变对象并不是真正不可变的。它们的内部属性和值可以通过反射来修改,但一般我们不这样做。

static关键字有什么用?谈谈你的理解。 static是Java中一个非常重要的关键字。 static所代表的概念就是静态的。 Java中主要使用static

修改变量。用static修饰的变量称为静态变量,也称为类变量。类变量属于类。对于不同的类,静态变量只有一份副本。 static修饰的变量位于方法区; static修饰的变量可以直接通过类名和变量名来访问,而不必实例化类然后使用。修饰的方法,静态修饰的方法称为静态方法。静态方法可以通过类名和方法名直接使用。非静态属性和方法不能在静态方法内使用。 static可以修改代码块,主要分为两种:一种是直接在类中定义,使用static{},称为静态代码块。另一种是在类中定义一个静态内部类,使用static class xxx来定义。 static 可用于通过import static xxx 静态导入包。一般不推荐这种方法。 static 可以与单例模式一起使用,通过双重检查锁实现线程安全的单例模式。详细信息请参考这篇文章。静态文章还能阻碍我吗?

Final关键字的用途是什么?谈谈你的理解。 Final是Java中的一个关键字。意思是不可改变的。在Java中,final主要用来

修饰的类,被final修饰的类不能被继承。这意味着你不能使用extends来继承被final修饰的类。修改变量。由final修饰的变量不能被重写。不被重写有两种含义。对于基本数据类型,被final修饰的变量的值是不能改变的。由final修饰的对象的引用不能被改变。但对象内部的属性是可以修改的。 Final修饰的变量在一定程度上具有不可变的作用,因此可以用来保护只读数据,尤其是在并发编程中,因为很明显final变量不能再被赋值,这有助于减少额外的同步。高架。修改后的方法、final修饰的方法不能被覆盖。 Final修饰符和Java程序性能优化没有必然联系。抽象类和接口有什么区别?抽象类和接口是Java中的关键字。抽象类和接口都允许定义方法,而不需要具体的方法实现。抽象类和接口都允许被继承,并且在JDK和框架的源代码中广泛使用它们来实现多态性和不同的设计模式。

区别在于

不同的抽象层次:类、抽象类、接口实际上是三种不同的抽象层次。抽象级别的顺序是接口、抽象类和类。接口中只允许定义方法,不允许方法的实现。方法的定义和实现可以在抽象类中进行;但类中只允许实现方法。我所说的方法的定义在方法中是不允许的。 {}后面使用的关键字不同:类用class表示;抽象类用抽象类来表示;接口由接口表示:接口中定义的变量只能是公共静态常量,抽象类中的变量是普通变量。重写和重载的区别在Java中,重写和重载是同一个方法的不同表达。让我们简单区分一下重写和重载。

孩子和父母之间的关系是不同的。重写是针对子类和父类的不同表达,而重载则是同一个类的不同表达。概念不同。子类重写父类的方法一般用@override表示。被重写方法的方法声明、参数类型和顺序必须与父类完全一致;重载是针对同一个类中的概念,它要求重载的方法必须满足以下任何一个要求:方法参数的顺序、参数的数量和参数的类型可以保持不同。字节的取值范围是多少?如何计算byte的取值范围?它在-128到127之间,总共256位。一个字节类型在计算机中占用一个字节,即8位,所以最大为2^8=1111 1111。

Java 使用二进制补码来表示二进制数。补码的最高位是符号位。最高位为0 表示正数,最高位1 表示负数。正数的补码是它本身。由于最高位是符号位,所以正数代表0111 1111,即127。负数最大为1111 1111,涉及两个0,一个+0,一个-0。 +0被归类为正数,即0,-0被归类为负数,即-128,所以字节的范围是-128-127。

HashMap 和HashTable 的异同

HashMap和HashTable都是基于哈希表实现的,里面的每个元素都是一个键值对。 HashMap 和HashTable 都实现了Map、Cloneable 和Serialized 接口。

不同之处

父类不同:HashMap继承AbstractMap类,而HashTable继承Dictionary类。空值不同:HashMap允许空的key和value值,而HashTable不允许空的key和value值。 HashMap 会将Null 键视为普通键。不允许重复空键。线程安全:HashMap不是线程安全的。如果多个外部操作同时修改HashMap的数据结构,比如添加、删除等,就必须进行同步操作。仅修改key或value并不是改变数据结构的操作。您可以选择构造一个线程安全的Map,例如Collections.synchronizedMap或ConcurrentHashMap。而HashTable本身就是一个线程安全的容器。性能:虽然HashMap和HashTable都是基于单链表,但HashMap对于put或get操作可以达到恒定时间的性能;而HashTable的put和get操作都是synchronized锁,所以效率很差。初始容量不同:HashTable初始长度为11,后续每次扩容容量变为之前的2n+1(n为之前的长度),而HashMap初始长度为16,后续每次扩容容量变为之前的2倍原始长度。创建时,如果给定了一个容量的初始值,HashTable会直接使用你给定的大小,而HashMap会将其扩展为2的幂。 HashMap和HashSet的区别HashSet继承自AbstractSet接口,实现了Set、Cloneable和java.io.Serialized 接口。 HashSet不允许集合中有重复的值。 HashSet的底层实际上是HashMap,对HashSet的所有操作实际上都是对HashMap的操作。所以HashSet不保证集合的顺序,也不是线程安全的容器。

在HashMap的底层结构JDK1.7中,HashMap采用位桶+链表的实现方式,即用链表来处理冲突,将哈希值相同的链表存储在一个数组中。但当桶中元素较多时,即哈希值相等的元素较多时,按键值顺序查找的效率较低。

因此,与JDK 1.7相比,JDK 1.8在底层结构上做了一些改变。当每个桶中的元素数量大于8时,将转换为红黑树,以优化查询效率。

这几天我一直在思考为什么HashMap的长度是2的幂。当我和群里的朋友讨论日常问题时,我问他们为什么length%hash==(n - 1) hash。他们相等的前提是长度取2的次方。然后我回答说,长度不能取2的次方吗?其实我没明白其中的因果关系,因为HashMap的长度是2的幂,所以用余数来确定桶中的下标。如果length的长度不是2的幂,朋友们可以举个例子试试。

比如长度为9时,3(9-1)=0、2(9-1)=0,都在0上,发生碰撞;

这会增加HashMap碰撞的概率。

HashMap多线程操作导致死循环问题。 HashMap 不是线程安全的容器。高并发场景下,应该使用ConcurrentHashMap。在多线程场景下使用HashMap会出现死循环问题(基于JDK1.7)。问题所在的位置是rehash地方,即

do { 条目下一个=e.next; //-- 假设线程一执行到这里就被安排挂起int i=indexFor(e.hash, newCapacity); e.next=newTable[i]; newTable[i]=e; e=下一个;} while (e !=null);这是JDK1.7的rehash代码片段,在并发场景下会形成循环。

JDK1.8也会导致死循环问题。

HashMap的线程安全实现有哪些?因为HashMap不是线程安全的容器,所以并发场景下建议使用ConcurrentHashMap,或者使用线程安全的HashMap或者Collections包下的线程安全容器,例如

Collections.synchronizedMap(new HashMap());还可以使用HashTable,它也是一个基于键值存储的线程安全容器。 HashMap和HashTable经常被比较,因为HashTable的数据结构与HashMap相同。

上面最高效的是ConcurrentHashMap。

下面说一下HashMap put的过程。首先使用哈希函数计算key,然后执行真正的插入方法。

Final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node[] tab;节点p;整数n,我; //如果表为null或者没有为表分配内存,则调整一次大小if ((tab=table)==null || (n=tab.length)==0) n=(tab=resize()) 。长度; //如果指定hash值的节点为空,则直接插入。这个(n - 1) Hash 是表中真正的哈希if ((p=tab[i=(n - 1) hash])==null) tab[i]=newNode(hash, key, value, null) ; //如果不为空else { 节点e; K k; //计算表中真实的hash值,与要插入的key.hash进行比较if (p.hash==hash ((k=p.key)==key || (key !=null key.equals (k)))) e=p; //如果不同,且当前节点已经在TreeNode上else if (p instanceof TreeNode) //使用红黑树存储方法e=((TreeNode)p).putTreeVal(this, tab, hash, key, value) ; //key.hash不同,不再在TreeNode上,在链表上找到p.next==null else { for ( int binCount=0; ++binCount) { if ((e=p.next)==null) { //在表末尾插入p.next=newNode(hash, key, value, null); //添加新节点后如果节点数达到阈值,则进入treeifyBin()再次判断if (binCount=TREEIFY_THRESHOLD - 1) //-1 for 1st treeifyBin(tab, hash);休息; } //如果找到hash和key相同的节点,则直接退出循环if (e.hash==hash ((k=e.key)==key || (key !=null key.equals(k) ))) 休息; //更新p 使其指向下一个节点p=e; } } //映射包含旧值,返回旧值if (e !=null) { //key 的现有映射V oldValue=e.value; if (!onlyIfAbsent || oldValue==null ) e.value=value; afterNodeAccess(e);返回旧值; } } //地图调整次数+ 1 ++modCount; //键值对数量达到阈值,需要扩容if (++size Threshold) resize() ; afterNodeInsertion(逐出); return null;} HashMap put方法的核心是putval方法。其插入过程如下

首先会判断HashMap是否是新建的。如果是,则先调整大小,然后判断要插入的元素是否已经存在于HashMap中(说明发生了碰撞)。如果不存在,则直接生成新的k-。

v 节点存放,再判断是否需要扩容。如果要插入的元素已经存在的话,说明发生了冲突,这就会转换成链表或者红黑树来解决冲突,首先判断链表中的 hash,key 是否相等,如果相等的话,就用新值替换旧值,如果节点是属于 TreeNode 类型,会直接在红黑树中进行处理,如果 hash ,key 不相等也不属于 TreeNode 类型,会直接转换为链表处理,进行链表遍历,如果链表的 next 节点是 null,判断是否转换为红黑树,如果不转换的话,在遍历过程中找到 key 完全相等的节点,则用新节点替换老节点ConcurrentHashMap 底层实现ConcurrentHashMap 是线程安全的 Map,它也是高并发场景下的首选数据结构,ConcurrentHashMap 底层是使用分段锁来实现的。 Integer 缓存池Integer 缓存池也就是 IntegerCache ,它是 Integer 的静态内部类。 它的默认值用于缓存 -128 - 127 之间的数字,如果有 -128 - 127 之间的数字的话,使用 new Integer 不用创建对象,会直接从缓存池中取,此操作会减少堆中对象的分配,有利于提高程序的运行效率。 例如创建一个 Integer a = 24,其实是调用 Integer 的 valueOf ,可以通过反编译得出这个结论 然后我们看一下 valueOf 方法 如果在指定缓存池范围内的话,会直接返回缓存的值而不用创建新的 Integer 对象。 缓存的大小可以使用 XX:AutoBoxCacheMax 来指定,在 VM 初始化时,java.lang.Integer.IntegerCache.high 属性会设置和保存在 sun.misc.VM 的私有系统属性中。 UTF-8 和 Unicode 的关系由于每个国家都有自己独有的字符编码,所以Unicode 的发展旨在创建一个新的标准,用来映射当今使用的大多数语言中的字符,这些字符有一些不是必要的,但是对于创建文本来说却是不可或缺的。Unicode 统一了所有字符的编码,是一个 Character Set,也就是字符集,字符集只是给所有的字符一个唯一编号,但是却没有规定如何存储,不同的字符其存储空间不一样,有的需要一个字节就能存储,有的则需要2、3、4个字节。 UTF-8 只是众多能够对文本字符进行解码的一种方式,它是一种变长的方式。UTF-8 代表 8 位一组表示 Unicode 字符的格式,使用 1 - 4 个字节来表示字符。 U+ 0000 ~ U+ 007F: 0XXXXXXXU+ 0080 ~ U+ 07FF: 110XXXXX 10XXXXXXU+ 0800 ~ U+ FFFF: 1110XXXX 10XXXXXX 10XXXXXXU+10000 ~ U+1FFFF: 11110XXX 10XXXXXX 10XXXXXX 10XXXXXX可以看到,UTF-8 通过开头的标志位位数实现了变长。对于单字节字符,只占用一个字节,实现了向下兼容 ASCII,并且能和 UTF-32 一样,包含 Unicode 中的所有字符,又能有效减少存储传输过程中占用的空间。 项目为 UTF-8 环境,char c = '中',是否合法可以,因为 Unicode 编码采用 2 个字节的编码,UTF-8 是 Unicode 的一种实现,它使用可变长度的字符集进行编码,char c = '中' 是两个字节,所以能够存储。合法。 Arrays.asList 获得的 List 应该注意什么Arrays.asList 是 Array 中的一个静态方法,它能够实现把数组转换成为 List 序列,需要注意下面几点 Arrays.asList 转换完成后的 List 不能再进行结构化的修改,什么是结构化的修改?就是不能再进行任何 List 元素的增加或者减少的操作。public static void main(String[] args) { Integer[] integer = new Integer[] { 1, 2, 3, 4 }; List integetList = Arrays.asList(integer); integetList.add(5);}结果会直接抛出 Exception in thread "main" java.lang.UnsupportedOperationException我们看一下源码就能发现问题 // 这是 java.util.Arrays 的内部类,而不是 java.util.ArrayList private static class ArrayList extends AbstractList implements RandomAccess, java.io.Serializable继承 AbstractList 中对 add、remove、set 方法是直接抛异常的,也就是说如果继承的子类没有去重写这些方法,那么子类的实例去调用这些方法是会直接抛异常的。 下面是AbstractList中方法的定义,我们可以看到具体抛出的异常: public void add(int index, E element) { throw new UnsupportedOperationException();}public E remove(int index) { throw new UnsupportedOperationException();}public E set(int index, E element) { throw new UnsupportedOperationException();}虽然 set 方法也抛出了一场,但是由于 内部类 ArrayList 重写了 set 方法,所以支持其可以对元素进行修改。 Arrays.asList 不支持基础类型的转换Java 中的基础数据类型(byte,short,int,long,float,double,boolean)是不支持使用 Arrays.asList 方法去转换的 Collection 和 Collections 的区别Collection 和 Collections 都是位于 java.util 包下的类 Collection 是集合类的父类,它是一个顶级接口,大部分抽象类比如说 AbstractList、AbstractSet 都继承了 Collection 类,Collection 类只定义一节标准方法比如说 add、remove、set、equals 等,具体的方法由抽象类或者实现类去实现。 Collections 是集合类的工具类,Collections 提供了一些工具类的基本使用 sort 方法,对当前集合进行排序, 实现 Comparable 接口的类,只能使用一种排序方案,这种方案叫做自然比较比如实现线程安全的容器 Collections.synchronizedList、 Collections.synchronizedMap 等reverse 反转,使用 reverse 方法可以根据元素的自然顺序 对指定列表按降序进行排序。fill,使用指定元素替换指定列表中的所有元素。有很多用法,读者可以翻阅 Collections 的源码查看,Collections 不能进行实例化,所以 Collections 中的方法都是由 Collections.方法 直接调用。 你知道 fail-fast 和 fail-safe 吗fail-fast 是 Java 中的一种快速失败机制,java.util 包下所有的集合都是快速失败的,快速失败会抛出 ConcurrentModificationException 异常,fail-fast 你可以把它理解为一种快速检测机制,它只能用来检测错误,不会对错误进行恢复,fail-fast 不一定只在多线程环境下存在,ArrayList 也会抛出这个异常,主要原因是由于 modCount 不等于 expectedModCount。 fail-safe 是 Java 中的一种 安全失败 机制,它表示的是在遍历时不是直接在原集合上进行访问,而是先复制原有集合内容,在拷贝的集合上进行遍历。 由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 ConcurrentModificationException。java.util.concurrent 包下的容器都是安全失败的,可以在多线程条件下使用,并发修改。 ArrayList、LinkedList 和 Vector 的区别这也是一道老生常谈的问题了 ArrayList、LinkedList、Vector 都是位于 java.util 包下的工具类,它们都实现了 List 接口。 ArrayList 的底层是动态数组,它是基于数组的特性而演变出来的,所以ArrayList 遍历访问非常快,但是增删比较慢,因为会涉及到数组的拷贝。ArrayList 是一个非线程安全的容器,在并发场景下会造成问题,如果想使用线程安全的容器的话,推荐使用 Collections.synchronizedList;ArrayList 在扩容时会增加 50% 的容量。LinkedList 的底层是双向链表,所以 LinkedList 的增加和删除非常快,只需把元素删除,把各自的指针指向新的元素即可。但是 LinkedList 遍历比较慢,因为只有每次访问一个元素才能知道下一个元素的值。LinkedList 也是一个非线程安全的容器,推荐使用 Collections.synchronizedListVector 向量是最早出现的集合容器,Vector 是一个线程安全的容器,它的每个方法都粗暴的加上了 synchronized 锁,所以它的增删、遍历效率都很低。Vector 在扩容时,它的容量会增加一倍。Exception 和 Error 有什么区别Exception 泛指的是 异常,Exception 主要分为两种异常,一种是编译期出现的异常,称为 checkedException ,一种是程序运行期间出现的异常,称为 uncheckedException,常见的 checkedException 有 IOException,uncheckedException 统称为 RuntimeException,常见的 RuntimeException 主要有NullPointerException、 IllegalArgumentException、ArrayIndexOutofBoundException等,Exception 可以被捕获。 Error 是指程序运行过程中出现的错误,通常情况下会造成程序的崩溃,Error 通常是不可恢复的,Error 不能被捕获。 详细可以参考这篇文章 看完这篇 Exception 和 Error ,和面试官扯皮就没问题了 String、StringBuilder 和 StringBuffer 有什么区别String 特指的是 Java 中的字符串,String 类位于 java.lang 包下,String 类是由 final 修饰的,String 字符串一旦创建就不能被修改,任何对 String 进行修改的操作都相当于重新创建了一个字符串。String 字符串的底层使用 StringBuilder 来实现的 StringBuilder 位于 java.util 包下,StringBuilder 是一非线程安全的容器,StringBuilder 的 append 方法常用于字符串拼接,它的拼接效率要比 String 中 + 号的拼接效率高。StringBuilder 一般不用于并发环境 StringBuffer 位于 java.util 包下,StringBuffer 是一个线程安全的容器,多线程场景下一般使用 StringBuffer 用作字符串的拼接 StringBuilder 和 StringBuffer 都是继承于AbstractStringBuilder 类,AbstractStringBuilder 类实现了 StringBuffer 和 StringBuilder 的常规操作。 动态代理是基于什么原理代理一般分为静态代理和 动态代理,它们都是代理模式的一种应用,静态代理指的是在程序运行前已经编译好,程序知道由谁来执行代理方法。 而动态代理只有在程序运行期间才能确定,相比于静态代理, 动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。可以说动态代理是基于 反射 实现的。通过反射我们可以直接操作类或者对象,比如获取类的定义,获取声明的属性和方法,调用方法,在运行时可以修改类的定义。 动态代理是一种在运行时构建代理、动态处理方法调用的机制。动态代理的实现方式有很多,Java 提供的代理被称为 JDK 动态代理,JDK 动态代理是基于类的继承。 int 和 Integer 的区别int 和 Integer 区别可就太多了 int 是 Java 中的基本数据类型,int 代表的是 整型,一个 int 占 4 字节,也就是 32 位,int 的初始值是默认值是 0 ,int 在 Java 内存模型中被分配在栈中,int 没有方法。Integer 是 Java 中的基本数据类型的包装类,Integer 是一个对象,Integer 可以进行方法调用,Integer 的默认值是 null,Integer 在 Java 内存模型中被分配在堆中。int 和 Integer 在计算时可以进行相互转换,int -> Integer 的过程称为 装箱,Integer -> int 的过程称为 拆箱,Integer 还有 IntegerCache ,会自动缓存 -128 - 127 中的值Java 提供了哪些 I/O 方式Java I/O 方式有很多种,传统的 I/O 也称为 BIO,主要流有如下几种 Java I/O 包的实现比较简单,但是容易出现性能瓶颈,传统的 I/O 是基于同步阻塞的。 JDK 1.4 之后提供了 NIO,也就是位于 java.nio 包下,提供了基于 channel、Selector、Buffer的抽象,可以构建多路复用、同步非阻塞 I/O 程序。 JDK 1.7 之后对 NIO 进行了进一步改进,引入了 异步非阻塞 的方式,也被称为 AIO(Asynchronous IO)。可以用生活中的例子来说明:项目经理交给手下员工去改一个 bug,那么项目经理不会一直等待员工解决 bug,他肯定在员工解决 bug 的期间给其他手下分配 bug 或者做其他事情,员工解决完 bug 之后再告诉项目经理 bug 解决完了。 谈谈你知道的设计模式一张思维导图镇场 比如全局唯一性可以用 单例模式。 可以使用 策略模式 优化过多的 if...else... 制定标准用 模版模式 接手其他人的锅,但不想改原来的类用 适配器模式 使用 组合 而不是继承 使用 装饰器可以制作加糖、加奶酪的咖啡 代理 可以用于任何中间商...... Comparator 和 Comparable 有什么不同Comparable 更像是自然排序Comparator 更像是定制排序同时存在时采用 Comparator(定制排序)的规则进行比较。 对于一些普通的数据类型(比如 String, Integer, Double…),它们默认实现了Comparable 接口,实现了 compareTo 方法,我们可以直接使用。 而对于一些自定义类,它们可能在不同情况下需要实现不同的比较策略,我们可以新创建 Comparator 接口,然后使用特定的 Comparator 实现进行比较。 Object 类中一般都有哪些方法Object 类是所有对象的父类,它里面包含一些所有对象都能够使用的方法 hashCode():用于计算对象的哈希码equals():用于对象之间比较值是否相等toString(): 用于把对象转换成为字符串clone(): 用于对象之间的拷贝wait(): 用于实现对象之间的等待notify(): 用于通知对象释放资源notifyAll(): 用于通知所有对象释放资源finalize(): 用于告知垃圾回收器进行垃圾回收getClass(): 用于获得对象类Java 泛型和类型擦除关于 Java 泛型和擦除看着一篇就够了。 反射的基本原理,反射创建类实例的三种方式是什么反射机制就是使 Java 程序在运行时具有自省(introspect) 的能力,通过反射我们可以直接操作类和对象,比如获取某个类的定义,获取类的属性和方法,构造方法等。 创建类实例的三种方式是 对象实例.getClass();通过 Class.forName() 创建对象实例.newInstance() 方法创建强引用、若引用、虚引用和幻象引用的区别我们说的不同的引用类型其实都是逻辑上的,而对于虚拟机来说,主要体现的是对象的不同的可达性(reachable) 状态和对垃圾收集(garbage collector)的影响。 可以通过下面的流程来对对象的生命周期做一个总结 对象被创建并初始化,对象在运行时被使用,然后离开对象的作用域,对象会变成不可达并会被垃圾收集器回收。图中用红色标明的区域表示对象处于强可达阶段。 JDK1.2 介绍了 java.lang.ref 包,对象的生命周期有四个阶段:强可达(Strongly Reachable)、软可达(Soft Reachable)、弱可达(Weak Reachable)、 幻象可达(Phantom Reachable)。 如果只讨论符合垃圾回收条件的对象,那么只有三种:软可达、弱可达和幻象可达。 软可达:软可达就是我们只能通过软引用才能访问的状态,软可达的对象是由 SoftReference 引用的对象,并且没有强引用的对象。软引用是用来描述一些还有用但是非必须的对象。垃圾收集器会尽可能长时间的保留软引用的对象,但是会在发生 OutOfMemoryError 之前,回收软引用的对象。如果回收完软引用的对象,内存还是不够分配的话,就会直接抛出 OutOfMemoryError。弱可达:弱可达的对象是 WeakReference 引用的对象。垃圾收集器可以随时收集弱引用的对象,不会尝试保留软引用的对象。幻象可达:幻象可达是由 PhantomReference 引用的对象,幻象可达就是没有强、软、弱引用进行关联,并且已经被 finalize 过了,只有幻象引用指向这个对象的时候。除此之外,还有强可达和不可达的两种可达性判断条件 强可达:就是一个对象刚被创建、初始化、使用中的对象都是处于强可达的状态不可达(unreachable):处于不可达的对象就意味着对象可以被清除了。下面是一个不同可达性状态的转换图 判断可达性条件,也是 JVM 垃圾收集器决定如何处理对象的一部分考虑因素。 所有的对象可达性引用都是 java.lang.ref.Reference 的子类,它里面有一个get() 方法,返回引用对象。 如果已通过程序或垃圾收集器清除了此引用对象,则此方法返回 null 。也就是说,除了幻象引用外,软引用和弱引用都是可以得到对象的。而且这些对象可以人为拯救,变为强引用,例如把 this 关键字赋值给对象,只要重新和引用链上的任意一个对象建立关联即可。 final、finally 和 finalize() 的区别这三者可以说是没有任何关联之处,我们上面谈到了,final 可以用来修饰类、变量和方法,可以参考上面 final 的那道面试题。 finally 是一个关键字,它经常和 try 块一起使用,用于异常处理。使用 try...finally 的代码块种,finally 部分的代码一定会被执行,所以我们经常在 finally 方法中用于资源的关闭操作。 JDK1.7 中,推荐使用 try-with-resources 优雅的关闭资源,它直接使用 try(){} 进行资源的关闭即可,就不用写 finally 关键字了。 finalize 是 Object 对象中的一个方法,用于对象的回收方法,这个方法我们一般不推荐使用,finalize 是和垃圾回收关联在一起的,在 Java 9 中,将 finalize 标记为了 deprecated, 如果没有特别原因,不要实现 finalize 方法,也不要指望他来进行垃圾回收。 内部类有哪些分类,分别解释一下在 Java 中,可以将一个类的定义放在另外一个类的定义内部,这就是内部类。内部类本身就是类的一个属性,与其他属性定义方式一致。 内部类的分类一般主要有四种 成员内部类局部内部类匿名内部类静态内部类静态内部类就是定义在类内部的静态类,静态内部类可以访问外部类所有的静态变量,而不可访问外部类的非静态变量; 成员内部类 就是定义在类内部,成员位置上的非静态类,就是成员内部类。成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和公有。 定义在方法中的内部类,就是局部内部类。定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法。 匿名内部类 就是没有名字的内部类,除了没有名字,匿名内部类还有以下特点: 匿名内部类必须继承一个抽象类或者实现一个接口匿名内部类不能定义任何静态成员和静态方法。当所在的方法的形参需要被匿名内部类使用时,必须声明为 final。匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。说出几种常用的异常NullPointerException: 空指针异常NoSuchMethodException:找不到方法IllegalArgumentException:不合法的参数异常IndexOutOfBoundException: 数组下标越界异常IOException:由于文件未找到、未打开或者I/O操作不能进行而引起异常ClassNotFoundException :找不到文件所抛出的异常NumberFormatException: 字符的UTF代码数据格式有错引起异常;InterruptedException: 线程中断抛出的异常静态绑定和动态绑定的区别一个Java 程序要经过编写、编译、运行三个步骤,其中编写代码不在我们讨论的范围之内,那么我们的重点自然就放在了编译 和 运行这两个阶段,由于编译和运行阶段过程相当繁琐,下面就我的理解来进行解释: Java 程序从源文件创建到程序运行要经过两大步骤: 1、编译时期是由编译器将源文件编译成字节码的过程 2、字节码文件由Java虚拟机解释执行 绑定绑定就是一个方法的调用与调用这个方法的类连接在一起的过程被称为绑定。 绑定主要分为两种: 静态绑定 和 动态绑定 绑定的其他叫法 静态绑定 == 前期绑定 == 编译时绑定 动态绑定 == 后期绑定 == 运行时绑定 为了方便区分: 下面统一称呼为静态绑定和动态绑定 静态绑定在程序运行前,也就是编译时期 JVM 就能够确定方法由谁调用,这种机制称为静态绑定 识别静态绑定的三个关键字以及各自的理解 如果一个方法由 private、static、final 任意一个关键字所修饰,那么这个方法是前期绑定的 构造方法也是前期绑定 private:private 关键字是私有的意思,如果被 private 修饰的方法是无法由本类之外的其他类所调用的,也就是本类所特有的方法,所以也就由编译器识别此方法是属于哪个类的 public class Person { private String talk; private String canTalk(){ return talk; }}class Animal{ public static void main(String[] args) { Person p = new Person(); // private 修饰的方法是Person类独有的,所以Animal类无法访问(动物本来就不能说话)// p.canTalk(); }}final:final 修饰的方法不能被重写,但是可以由子类进行调用,如果将方法声明为 final 可以有效的关闭动态绑定 public class Fruit { private String fruitName; final String eatingFruit(String name){ System.out.println("eating " + name); return fruitName; }}class Apple extends Fruit{ // 不能重写final方法,eatingFruit方法只属于Fruit类,Apple类无法调用// String eatingFruit(String name){// super.eatingFruit(name);// } String eatingApple(String name){ return super.eatingFruit(name); }}static: static 修饰的方法比较特殊,不用通过 new 出某个类来调用,由类名.变量名直接调用该方法,这个就很关键了,new 很关键,也可以认为是开启多态的导火索,而由类名.变量名直接调用的话,此时的类名是确定的,并不会产生多态,如下代码: public class SuperClass { public static void sayHello(){ System.out.println("由 superClass 说你好"); }}public class SubClass extends SuperClass{ public static void sayHello(){ System.out.println("由 SubClass 说你好"); } public static void main(String[] args) { SuperClass.sayHello(); SubClass.sayHello(); }}SubClass 继承 SuperClass 后,在 是无法重写 sayHello 方法的,也就是说 sayHello() 方法是对子类隐藏的,但是你可以编写自己的 sayHello() 方法,也就是子类 SubClass 的sayHello() 方法,由此可见,方法由 static 关键词所修饰,也是编译时绑定 动态绑定在运行时根据具体对象的类型进行绑定 除了由 private、final、static 所修饰的方法和构造方法外,JVM 在运行期间决定方法由哪个对象调用的过程称为动态绑定 如果把编译、运行看成一条时间线的话,在运行前必须要进行程序的编译过程,那么在编译期进行的绑定是前期绑定,在程序运行了,发生的绑定就是后期绑定 public class Father { void drinkMilk(){ System.out.println("父亲喜欢喝牛奶"); }}public class Son extends Father{ @Override void drinkMilk() { System.out.println("儿子喜欢喝牛奶"); } public static void main(String[] args) { Father son = new Son(); son.drinkMilk(); }}Son 类继承 Father 类,并重写了父类的 dringMilk() 方法,在输出结果得出的是儿子喜欢喝牛奶。那么上面的绑定方式是什么呢? 上面的绑定方式称之为动态绑定,因为在你编写 Father son = new Son() 的时候,编译器并不知道 son 对象真正引用的是谁,在程序运行时期才知道,这个 son 是一个 Father 类的对象,但是却指向了 Son 的引用,这种概念称之为多态,那么我们就能够整理出来多态的三个原则: 继承重写父类引用指向子类对象也就是说,在 Father son = new Son() ,触发了动态绑定机制。 动态绑定的过程 虚拟机提取对象的实际类型的方法表;虚拟机搜索方法签名;调用方法。动态绑定和静态绑定的特点静态绑定 静态绑定在编译时期触发,那么它的主要特点是 1、编译期触发,能够提早知道代码错误 2、提高程序运行效率 动态绑定 1、使用动态绑定的前提条件能够提高代码的可用性,使代码更加灵活。 2、多态是设计模式的基础,能够降低耦合性。

猜你喜欢