Java面试题总结

java面试题总结

Java基础篇

java有哪些特点

  • 并发性的︰你可以在其中执行许多语句,而不必一次执行它
  • 面向对象的︰基于类和面向对象的编程语言。
  • 独立性的︰支持一次编写,到处运行的独立编程语言,即编译后的代码可以在支持Java的所有平台上运行。

java的特征

java的特性有以下几点:

  • 简单,Java 会让你的工作变得更加轻松,使你把关注点放在主要业务逻辑上,而不必关心指针、运算符重载、内存回收等与主要业务无关的功能。
  • 便携性,Java是平台无关性的,这意味着在一个平台上编写的任何应用程序都可以轻松移植到另一个平台上。
  • 安全性,编译后会将所有的代码转换为字节码,人类无法读取。它使开发无病毒,无篡改的系统/应用成为可能。
  • 动态性,它具有适应不断变化的环境的能力,它能够支持动态内存分配,从而减少了内存浪费,提高了应用程序的性能。
  • 分布式,Java提供的功能有助于创建分布式应用。使用远程方法调用(RMI),程序可以通过网络调用另一个程序的方法并获取输出。您可以通过从互联网上的任何计算机上调用方法来访问文件。这是革命性的一个特点,对于当今的互联网来说太重要了。
  • 健壮性,Java有强大的内存管理功能,在编译和运行时检查代码,它有助于消除错误。
  • 高性能,Java最黑的科技就是字节码编程,Java代码编译成的字节码可以轻松转换为本地机器代码。通过JIT即时编译器来实现高性能。
  • 解释性, Java被编译成字节码,由Java运行时环境解释。
  • 多线程性,Java支持多个执行线程(也称为轻量级进程),包括一组同步原语。这使得使用线程编程更加容易,Java通过管程模型来实现线程安全性。

描述一下值传递和引用传递的区别

  • 值传递是指在调用函数时将实际参数复制一份到函数中,这样的话如果函数对其传递过来的形式参数进行修改,将不会影响到实际参数
  • 引用传递是指在调用函数时将对象的地址直接传递到函数中,如果在对形式参数进行修改,将影响到实际参数的值。

==和equals区别是什么

== 是Java中—种操作符,它有两种比较方式

  • 对于基本数据类型来说,==判断的是两边的值是否相等
  • 对于引用类型来说, == 判断的是两边的引用是否相等,也就是判断两个对象是否指向了同一块内存区域。
public class DoubleCompareAndEquals {

  public class Test {
  public static void main(String[] args) {
    Person person1 = new Person("张三",24);
    Person person2 = new Person("张三",24);
    Person person3 = person1;
    int a = 10;
    int b = 10;
    int c = 10;
    System.out.println(a == b);
    System.out.println(a == c);
    System.out.println(person1 == person2);
    System.out.println(person1 == person3);
 }
}

运行结果:
在这里插入图片描述

equals是Java 中所有对象的父类,即0bject类定义的一个方法。它只能比较对象,它表示的是引用双方的值是否相等。所以记住,并不是说==比较的就是引用是否相等,equals 比较的就是值,这需要区分来说的。

private void equals(){
System.out.println(person1.getName().equals(person2.getName()));
}

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 字符串的方法都是创建了一个新的字符串。
equals方法是Object类定义的方法,Object 是所有类的父类,当然也包括String, String 重写了equals方法,下面我们来看看是怎么重写的

public boolean equals (Object anObject) {
  if (this == an0bject) {
 return true ;
 }
 if (anobject instanceof String) {
    String anotherString = (String) an0bject;
    int n = value.length;
    if (n == anotherString.value.length) {
      char v1[] = value;
      char v2[] = anotherString.value;
      int i = 0;
      while (n-- != 0) {
        if (v1[i] != v2[i]) {
          return false;
       }
        i++;
      return true ;
   }
 }
  return false;
}
  • 首先会判断要比较的两个字符串它们的引用是否相等。如果引用相等的话,直接返回true,不相等的话继续下面的判断
  • 然后再判断被比较的对象是否是String的实例,如果不是的话直接返回false,如果是的话,再比较两个字符串的长度是否相等,如果长度不想等的话也就没有比较的必要了;长度如果相同,会比较字符串中的每个字符是否相等,一旦有一个字符不相等,就会直接返回false。
    具体流程图:
    在这里插入图片描述
    这里再提示一下,你可能有疑惑什么时候是
if (this == anObject) {
return true;
}

这个判断语句如何才能返回true?因为都是字符串啊,字符串比较的不都是堆空间吗,猛然一看发现好像永远也不会走,但是你忘记了String. intern()方法, 它表示的概念在不同的JDK版本有不同的区分
在JDK1.7及以后调用intern方法是判断运行时常量池中是否有指定的字符串,如果没有的话,就把字符串添加到常量池中,并返回常量池中的对象。
验证过程如下:

private void StringOverrideEquals(){
String s1 = "aaa";
String s2 = "aa" + new String("a");
String 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方法是用来判断每个对象hash 值的一种方法。如果只重写equals方法而不重写hashcode方法,很可能会造成两个不同的对象,它们的hashcode 也相等,造成冲突。比如

String 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为什么是不可变的、jdk 源码中的String如何定义的、为什么这么设计

首先了解一下什么是不可变对象,不可变对象就是一经创建后,其对象的内部状态不能被修改,啥意思呢?也就是说不可变对象需要遵守下面几条原则

  • 不可变对象的内部属性都是final 的
  • 不可变对象的内部属性都是private的
  • 不可变对象不能提供任何可以修改内部状态的方法、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可以修饰代码块,主要分为两种,-种直接定义在类中,使用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 来表示;抽象类使用abstract class 来表示; 接口使用interface来表示
  • 变量:接口中定义的变量只能是公共的静态常量,抽象类中的变量是普通变量。

重写和重载的区别

在Java中,重写和重载都是对同- -方法的不同表现形式,下 面我们针对重写和重载做一下简单的区分

  • 子父级关系不同,重写是针对子级和父级的不同表现形式,而重载是在同一类中的不同表现形式;
  • 概念不同,子类重写父类的方法- -般使用@override 来表示;重写后的方法其方法的声明和参数类型、顺序必须要与父类完全一致;重载是针对同- -类中概念,它要求重载的方法必须满足下面任何一个要求:方法参数的顺序,参数的个数,参数的类型任意一个保持不同即可。

byte的取值范围是多少,怎么计算出来的

byte的取值范围是-128-> 127之间,-共是256个。一个byte类型在计算机中占据一个字节,那么就是8bit,所以最大就是2^7 = 1111 1111。
Java中用补码来表示二进制数,补码的最高位是符号位,最高位用0表示正数,最高位1表示负数,正数的补码就是其本身,由于最高位是符号位,所以正数表示的就是0111 1111,也就是127。最大负数就是1111 1111,这其中会涉及到两个0,一个+0,一个-0,+0归为正数,也就是0, -0归为负数,也就
是-128,所以byte的范围就是-128 - 127。

HashMap和HashTable的区别

相同点
HashMap和HashTable都是基于哈希表实现的,其内部每个元素都是key-value 键值对,
HashMap和HashTable都实现了Map、Cloneable、 Serializable 接口。

不同点

  • 父类不同: HashMap继承了AbstractMap 类,而HashTable继承了Dictionary 类
    在这里插入图片描述

  • 空值不同: HashMap 允许空的key和value值,HashTable 不允许空的key和value值。
    HashMap会把Null key当做普通的key对待。不允许null key重复。
    在这里插入图片描述

  • 线程安全性: HashMap 不是线程安全的,如果多个外部操作同时修改HashMap的数据结构比如add或者是delete,必须进行同步操作,仅仅对key或者value的修改不是改变数据结构的操作。可以选择构造线程安全的Map比如Collections.synchronizedMap或者是
    ConcurrentHashMap。而HashTable本身就是线程安全的容器。

  • 性能方面:虽然HashMap和HashTable都是基于单链表的,但是HashMap进行put或者get操作,可以达到常数时间的性能;而HashTable的put和get操作都是加synchronized 锁的,所以效率很差。

在这里插入图片描述

  • 初始容量不同: HashTable 的初始长度是11,之后每次扩充容量变为之前的2n+1 (n为上一次的长度)而HashMap的初始长度为16,之后每次扩充变为原来的两倍。创建时,如果给定了容量初始值,那么HashTable 会直接使用你给定的大小,而HashMap会将其扩充为2的幂次方大小。

HashMap和HashSet的区别

HashSet继承于AbstractSet接口,实现了Set. Cloneable,. java.io.Serializable 接口。HashSet 不允许集合中出现重复的值。HashSet 底层其实就是HashMap,所有对HashSet的操作其实就是对HashMap的操作。所以HashSet也不保证集合的顺序,也不是线程安全的容器。

HashMap的底层结构

在这里插入图片描述
所以,与JDK 1.7相比,JDK 1.8在底层结构方面做了一些改变,当每个桶中元素大于8的时候,会转变为红黑树,目的就是优化查询效率。
在这里插入图片描述

HashMap的长度为什么是2的幂次方

这道题我想了几天,之前和群里小伙伴们探讨每8一题的时候, 问他们为什么length%hash== (n-1) & hash,它们说相等的前提是length的长度2的幂次方,然后我回了一句难道length还能不是2的幂次方吗?其实是我没有搞懂因果关系,因为HashMap的长度是2的冪次方,所以使用余数来判断在桶中的下标。如果length的长度不是2的幂次方,小伙伴们可以举个例子来试试

例如长度为9时候, 3& (9-1)=0,2&(9-1)=0, 都在0上,碰撞了;

这样会增大HashMap碰撞的几率。

HashMap多线程操作导致死循环问题

HashMap不是一个线程安全的容器,在高并发场景下,应该使用ConcurrentHashMap,
在多线程场景下使用HashMap会造成死循环问题(基于JDK1.7),出现问题的位置在rchash 处, 也就是

do {
Entry<K,V> next = e.next; // <--假设线程一执行到这里就被调度挂起了
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null)

这是JDK1.7的rehash代码片段,在并发的场景下会形成环。
JDK1.8也会造成死循环问题。

HashMap线程安全的实现有哪些

因为HashMap不是一个线程安全的容器,所以并发场景下推荐使用ConcurrentHashMap ,或者使用线程安全的HashMap,使用Collections 包下的线程安全的容器,比如说

Collections.synchronizedMap(new HashMapO);

还可以使用HashTable,它也是线程安全的容器,基于key-value 存储,经常用HashMap和HashTable做比较就是因为HashTable的数据结构和HashMap相同。
上面效率最高的就是ConcurrentHashMap.

讲一下HashMap put的过程

首先会使用hash函数来计算key,然后执行真正的插入方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
       boolean evict) {
  Node<K,V>[] tab; Node<K,V> p; int n, i;
  // 如果table 为 null 或者没有为table分配内存,就resize一次
  if ((tab = table) == null || (n = tab.length) == 0) {
    n = (tab = resize()).length;
 }
  // 指定hash值节点为空则直接插入,这个(n一1) & hash才是表中真正的哈希
  if ((p = tab[i = (n - 1) & hash]) == null) {
    tab[i] = newNode(hash, key, value, null);
 }
  // 如果不为空
  else {
    Node<K,V> e; K k;
    // 计算表中的这个真正的哈希值与要插入的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<K,V>)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);
          break;
       }
        // 如果找到了同hash、 key的节点,那么直接退出循环
        if (e.hash == hash &&
       ((k = e.key) == key || (key != null && key.equals(k)))) {
          break;
       }

        // 更新p指向下一节点
        p = e;
   }
 }
    // map中含有旧值,返回旧值
    if (e != null) { // existing mapping for key
      V oldValue = e.value;
      if (!onlyIfAbsent || oldValue == null)
      e.value = value;
      afterNodeAccess(e);
      return oldValue;
   }
 }
  // map调整次数+ 1
  ++modCount;
  // 键值对的数量达到阈值,需要扩容
  if (++size > threshold) {
    resize();
 }
  afterNodeInsertion(evict);
  return null;
}

HashMap put方法的核心就是在putval 方法, 它的插入过程如下

  • 首先会判断HashMap中是否是新构建的,如果是的话会首先进行resize, 然后判断需要插入的元素在HashMap中是否已经存在(说明出现了碰撞情况),如果不存在,直接生成新的k-v节点存放,再判断是否需要扩容。
  • 如果要插入的元素已经存在的话,说明发生了冲突,这就会转换成链表或者红黑树来解决冲突,首先判断链表中的hash, key 是否相等,如果相等的话,就用新值替换旧值,如果节点是属于TreeNode类型,会直接在红黑树中进行处理,如果hash ,key不相等也不属于TreeNode类型,会直接转换为链表处理,进行链表遍历,如果链表的next节点是nll,判断是否转换为红黑树,如果不转换的话,在遍历过程中找到key完全相等的节点,则用新节点替换老节点

ConcurrentHashMap底层实现

ConcurrentHashMap是线程安全的Map,它也是高并发场景下的首选数据结构,
ConcurrentHashMap底层是使用分段锁来实现的。

Integer缓存池

Integer缓存池也就是IntegerCache,它是Integer 的静态内部类。
在这里插入图片描述
它的默认值用于缓存-128- 127之间的数字,如果有-128 - 127之间的数字的话,使用new Integer不
用创建对象,会直接从缓存池中取,此操作会减少堆中对象的分配,有利于提高程序的运行效率。
例如创建一个Integera= 24,其实是调用Integer的value0f ,可以通过反编译得出这个结论
在这里插入图片描述
然后我们看一下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: 0XXXXXXX
U + 0080 ~ U + 07FF: 110XXXXX 10XXXXXX
U + 0800 ~ U + FFFF: 1110XXXX 10XXXXXX 10XXXXXX
U + 10000 ~ U + 1FFFF: 11110XXX 10XXXXXX 10XXXXXX 10XXXXX

可以看到,UTF-8 通过开头的标志位位数实现了变长。对于单字节字符,只占用-一个字节,实现了向下兼容ASCII,并且能和UTF-32一样,包含Unicode中的所有字符,又能有效减少存储传输过程中占用的空间。

项目为UTF-8环境,char c = ‘中’,是否合法

可以,因为Unicode编码采用2个字节的编码,UTF-8是Unicode的一种实现,它使用可变长度的字符集进行编码,char c = '中’是两个字节,所以能够存储。合法。

Arrays.asList获得的List应该注意什么

Arrays .asList是Array 中的一个静态方法,它能够实现把数组转换成为List 序列,需要注意下面几

  • Arays.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<E> extends AbstractList<E>
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方法可以根据元素的自然顺序对指定列表按降序进行排序。
  • fll, 使用指定元素替换指定列表中的所有元素。

有很多用法,读者可以翻阅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的底层是动态数组,它是基于数组的特性而演变出来的,所以ArayL ist遍历访问非常
    快,但是增删比较慢,因为会涉及到数组的拷贝。ArrayList 是一个非线程安全的容器, 在并
    发场景下会造成问题,如果想使用线程安全的容器的话,推荐使用Collections.synchronizedList ; ArrayList 在扩容时会增加50%的容量。
  • LinkedList的底层是双向链表,所以LinkedList的增加和删除非常快,只需把元素删除,把各
    自的指针指向新的元素即可。但是LinkedList遍历比较慢,因为只有每次访问-个元素才能知道
    下一个元素的值。LinkedList 也是一个非线程安全的容器, 推荐使用Collections.synchronizedList
  • Vector向量是最早出现的集合容器,Vector是一 个线程安全的容器,它的每个方法都粗暴的加
    上了synchronized 锁,所以它的增删、遍历效率都很低。Vector在扩容时,它的容量会增加一倍。

Exception和Error有什么区别

Exception泛指的是异常,Exception 主要分为两种异常,一种是编译期出现的异常,称为checkedException, 一种是程序运行期间出现的异常,称为uncheckedException

常见的checkedExceptionIOException ,uncheckedException 统称为RuntimeException

常见的RuntimeException主要有NullPointerException、IllegalArgumentException、
ArrayIndexOutofBoundException
等,Exception 可以被捕获。

Error是指程序运行过程中出现的错误,通常情况下会造成程序的崩溃,Error 通常是不可恢复的,
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 /0方式有很多种,传统的/O也称为BIO ,主要流有如下几种
在这里插入图片描述
Java I/0包的实现比较简单,但是容易出现性能瓶颈,传统的/0是基于同步阻塞的。
JDK 1.4之后提供了NIO, 也就是位于java.nio 包下,提供了基于channel. Selector、 Buffer
的抽象,可以构建多路复用、同步非阻塞/0程序。
JDK 1.7之后对NIO进行了进一步改进,引入了异步非阻塞的方式,也被称为
AIO(AsynchronousI0)。可以用生活中的例子来说明:项目经理交给手下员工去改一个bug,那么项目经理不会一直等待员工解决bug,他肯定在员工解决bug的期间给其他手下分配bug或者做其他事情,员工解决完bug之后再告诉项目经理bug解决完了。

谈谈你知道的设计模式

在这里插入图片描述

比如全局唯一性可以用单例模式
可以使用策略模式优化过多的f.else...
制定标准用模版模式
接手其他人的锅,但不想改原来的类用适配器模式
使用组合而不是继承
使用装饰器可以制作加糖、加奶酪的咖啡
代理可以用于任何中间商

Comparator和Comparable有什么不同

  • Comparable更像是自然排序
  • Comparator更像是定制排序
    同时存在时采用Comparator (定制排序)的规则进行比较。
    对于一些普 通的数据类型(比如String, Integer, Dolel…),它们默认实现了Comparable接口,实现了compareTo方法,我们可以直接使用。
    而对于一些自定义类,它们可能在不同情况下需要实现不同的比较策略,我们可以新创建
    Comparator接口,然后使用特定的Comparator实现进行比较。

Object类中一般都有哪些方法

Object类是所有对象的父类,它里面包含-些所有对象都能够使用的方法

hashCode0: 用于计算对象的哈希码
	equals0: 用于对象之间比较值是否相等
	toString0: 用于把对象转换成为字符串
	clone0: 用于对象之间的拷贝
	wait():用于实现对象之间的等待
	notify0:用于通知对象释放资源
	notifAlI(: 用于通知所有对象释放资源
	finalize0: 用于告知垃圾回收器进行垃圾回收
	getClass0:用于获得对象类

Java泛型和类型擦除

Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。理解类型擦除对于用好泛型是很有帮助的,尤其是一些看起来“疑难杂症”的问题,弄明白了类型擦除也就迎刃而解了。
泛型的类型擦除原则是:

  • 消除类型参数声明,即删除<>及其包围的部分。
  • 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配
    符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的
    最左边限定类型(即父类)。
  • 为了保证类型安全,必要时插入强制类型转换代码。
  • 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。

1 擦除类定义中的类型参数

1.1 无限制类型擦除
当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object,参见1。
在这里插入图片描述
1.2 有限制类型擦除
当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下
界,比如形如和<? extends Number>的类型参数被替换为Number,<? super Number>被替
换为Object,参见2。
在这里插入图片描述

2 擦除方法定义中的类型参数

擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,这里仅以擦除方法定义
中的有限制类型参数为例,见3。
在这里插入图片描述

3 桥接方法和泛型的多态

考虑下面的代码:

public interface Info&lt;T&gt; { 
  // just return var:-) 
  T info(T var);
}
public class BridgeMethodTest implements Info&lt;Integer&gt; { 
  @Override 
  public Integer info(Integer var) {  
    return var; 
 }
}

按照我们之前类型擦除的经验,在擦除类型后的代码应该是这个样子的:

public interface Info { 
  // just return var 
  Object info(Object var);
}
public class BridgeMethodTest implements Info { 
  @Override 
  public Integer info(Integer var) {  
    return var; 
 }
}

但是,明显可以看出,这样擦除类型后的代码在语法上是错误的:BridgeMethodTest类中虽
然存在一个info方法,但是和Info接口要求覆盖的info方法不一致:参数类型不一致。在这种
情况下,Java编译器会自动增加一个所谓的“桥接方法”(bridge method)来满足Java语法的
要求,同时也保证了基于泛型的多态能够有效。我们反编译一下BridgeMethodTest.class文件
可以看到Java编译器到底是如何做的:

$ javap BridgeMethodTest.class
Compiled from “BridgeMethodTest.java”
public class BridgeMethodTest implements Info<java.lang.Integer> {
public BridgeMethodTest();
public java.lang.Integer info(java.lang.Integer);
public java.lang.Object info(java.lang.Object);
}

可以看出,Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为
Object的info方法,参数为Object的info方法就是“桥接方法”。如何理解“桥接”二字呢?我们进
一步反编译BridgeMethodTest看一下:

// Decompiled by Jad v1.5.8e. Copyright 2001 Pavel Kouznetsov.   //
Jad home page: http://www.geocities.com/kpdus/jad.html
// Decompiler options: packimports(3)
// Source File Name: BridgeMethodTest.java 
public class BridgeMethodTest 
  implements Info{ 
  public BridgeMethodTest() { 
 }  
  public Integer info(Integer integer) {   
    return integer; 
 }  
  public volatile Object info(Object obj) {   
    return info((Integer)obj); 
 } 
}

info(Object)方法通过调用子类的info(Integer)方法搭起了父类和子类的桥梁,也就是说,info(Object obj)这个方法起到了连接父类和子类的作用,使得Java的多态在泛型情况下依然有效。
当然,我们在使用基于泛型的多态时不必过多的考虑“桥接方法”,Java编译器会帮我们打理好
一切。

反射的基本原理,反射创建类实例的三种方式是什么

反射机制就是使Java程序在运行时具有自省(introspect) 的能力,通过反射我们可以直接操作类和对象,比如获取某个类的定义,获取类的属性和方法,构造方法等。
创建类实例的三种方式是

  • 对象实例.getClass);
  • 通过Class.forName(创建
  • 对象实例.newInstance(方法创建

强引用、若引用、虚引用和幻象引用的区别

我们说的不同的引用类型其实都是逻辑上的,而对于虚拟机来说,主要体现的是对象的不同的可达性(reachable)状态和对垃圾收集( garbage collector) 的影响。
可以通过下面的流程来对对象的生命周期做一个总结
在这里插入图片描述
对象被创建并初始化,对象在运行时被使用,然后离开对象的作用域,对象会变成不可达并会被垃
圾收集器回收。图中用红色标明的区域表示对象处于强可达阶段。
JDK1.2介绍了java.lang.ref 包, 对象的生命周期有四个阶段:强可达(Strongly Reachable)、软可达(Soft Reachable) 、弱可达(WeakReachable)、 幻象可达(Phantom Reachable)。
在这里插入图片描述
如果只讨论符合垃圾回收条件的对象,那么只有三种:软可达、弱可达和幻象可达。

  • 软可达: 软可达就是我们只能通过软引用才能访问的状态,软可达的对象是由SoftReference引
    用的对象,并且没有强引用的对象。软引用是用来描述一些还有用但是非必须的对象。垃圾收集器会尽可能长时间的保留软引用的对象,但是会在发生OutOfMemoryError 之前,回收软引用的对象。如果回收完软引用的对象,内存还是不够分配的话,就会直接抛出OutOfMemoryError。
  • 弱可达:弱可达的对象是WeakReference 引用的对象。垃圾收集器可以随时收集弱引用的对象,不会尝试保留软引用的对象。幻象可达:幻象可达是由PhantomReference 引用的对象,幻象可达就是没有强、软、弱引用进行关联,并且已经被finalize过了,只有幻象引用指向这个对象的时候。除此之外,还有强可达和不可达的两种可达性判断条件
  • 强可达:就是一个对象刚被创建、初始化、使用中的对象都是处于强可达的状态不可达(unreachable) :处于不可达的对象就意味着对象可以被清除了。
    下面是一个不同可达性状态的转换图
    在这里插入图片描述
    判断可达性条件,也是JVM垃圾收集器决定如何处理对象的一部分考虑因素。
    所有的对象可达性引用都是java. lang. ref . Reference的子类,它里面有一个get() 方法,返回引用对象。 如果已通过程序或垃圾收集器清除了此引用对象,则此方法返回null。也就是说,除了幻象引用外,软引用和弱引用都是可以得到对象的。而且这些对象可以人为拯救,变为强引用,例如把this关键字赋值给对象,只要重新和引用链上的任意一个对 象建立关联即可。

final、finally 和finalize0的区别

这三者可以说是没有任何关联之处,我们上面谈到了,final 可以用来修饰类、变量和方法,可以参
考上面final的那道面试题。
finally是一个关键字,它经常和try块一起使用,用于异常处理。使用tr…nallyl的代码块种,finally部分的代码一定会被执行,所以我们经常在finally 方法中用于资源的关闭操作。
JDK1.7中,推荐使用try-with-resources 优雅的关闭资源,它直接使用try0{} 进行资源的关闭即可,就不用写finally 关键字了。
finalize是Object对象中的一个方法,用于对象的回收方法,这个方法我们一般不推荐使用,finalize是和垃圾回收关联在一起的,在Java9中,将finalize标记为了deprecated ,如果没有特别原因,不要实现finalize 方法,也不要指望他来进行垃圾回收。

内部类有哪些分类,分别解释一下

在Java中,可以将-个类的定义放在另外-个类的定义内部,这就是内部类。内部类本身就是类的一个
属性,与其他属性定义方式一致。
内部类的分类一般主 要有四种.

成员内部类
局部内部类
匿名内部类
静态内部类

静态内部类就是定义在类内部的静态类,静态内部类可以访问外部类所有的静态变量,而不可访问外部类的非静态变量;
成员内部类就是定义在类内部,成员位置上的非静态类,就是成员内部类。成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和公有。
定义在方法中的内部类,就是局部内部类。定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法。

  • 匿名内部类就是没有名字的内部类,除了没有名字,匿名内部类还有以下特点:
  • 匿名内部类必须继承一个抽象类或者实现-个接口
  • 匿名内部类不能定义任何静态成员和静态方法。
  • 当所在的方法的形参需要被匿名内部类使用时,必须声明为final。
  • 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。

说出几种常用的异常

NullPointerException: 空指针异常
NoSuchMethodException: 找不到方法
llegalArgumentException: 不合法的参数异常.
IndexOutOfBoundException: 数组下标越界异常
lOException: 由于文件未找到、未打开或者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的sayHello0方法,由此可见,方法由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 Son0,触发了动态绑定机制。

动态绑定的过程

1.虚拟机提取对 象的实际类型的方法表;
2.虚拟机搜索方法签名;
3.调用方法。

动态绑定和静态绑定的特点

静态绑定
静态绑定在编译时期触发,那么它的主要特点是

1、编译期触发,能够提早知道代码错误
2、提高程序运行效率

动态绑定

1、使用动态绑定的前提条件能够提高代码的可用性,使代码更加灵活。
2、多态是设计模式的基础,能够降低耦合性。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值