Java面经整理

腾讯

1.java基础

  • 8种基本数据类型,int几个字节

    类型

    存储需求

    取值范围

    byte

    1B

    -128~127

    short

    2B

    -32768~32767

    int

    4B

    -20亿~20亿

    long

    8B

    float

    4B

    小数点后6~7位

    double

    8B

    小数点后15位

    char

    2B

    boolean

    true/false

  • static,final关键字

    • static
      • 被static修饰的成员变量和成员方法独立于该类的任何对象。也就是说,它不依赖类特定的实例,被类的所有实例共享。只要这个类被加载了,Java虚拟机就能根据类名在运行时数据区的方法区内定找到他们。

      • **静态变量(**类变量):静态变量被所有的对象所共享,也就是说我们创建了一个类的多个对象,多个对象共享着一个静态变量,如果我们修改了静态变量的值,那么其他对象的静态变量也会随之修改。

        非静态变量(实例变量):如果我们创建了一个类的多个对象,那么每个对象都有它自己该有的非静态变量。当你修改其中一个对象中的非静态变量时,不会引起其他对象非静态变量值得改变。

      • static修饰的成员方法称作静态方法,这样我们就可以通过“类名**.**方法名”进行调用。由于静态方法在类加载的时候就存在了,所以它不依赖于任何对象的实例就可以进行调用,因此对于静态方法而言,是木有当前对象的概念,即没有this、super关键字的。因为static方法独立于任何实例,因此static方法必须被实现,而不能是抽象的abstract。

      • 被static修饰的代码块也叫静态代码块,会随着JVM加载类的时候而加载这些静态代码块,并且会自动执行。它们可以有多个,可以存在于该类的任何地方。JVM会按照它们的先后顺序依次执行它们,而且每个静态代码块只会被初始化一次,不会进行多次初始化。

      • 普通类是不允许声明为静态的,只有内部类才可以,被static修饰的内部类可以直接作为一个普通类来使用,而不需先实例一个外部类

        • 静态内部类只能访问外部类的静态成员,否则编译会报错
        • 不管是静态方法还是非静态方法都可以在非静态内部类中访问。
        • 如果需要调用内部类的非静态方法,必须先new一个OuterClass的对象outerClass,然后通过outer。new生成内部类的对象,而static内部类则不需要。
    • final
      • 修饰类:当用final修饰一个类时,表明这个类不能被继承。
      • 修饰方法:方法不能被重写(可以重载多个final修饰的方法)。此处需要注意的一点是:因为重写的前提是子类可以从父类中继承此方法,如果父类中final修饰的方法同时访问控制权限为private,将会导致子类中不能直接继承到此方法,因此,此时可以在子类中定义相同的方法名和参数,此时不再产生重写与final的矛盾,而是在子类中重新定义了新的方法。(注:类的private方法会隐式地被指定为final方法。)
      • 修饰变量:当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化;如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。本质上是一回事,因为引用的值是一个地址,final要求值,即地址的值不发生变化。**final修饰一个成员变量(属性),必须要显示初始化。**这里有两种初始化方式,一种是在变量声明的时候初始化;第二种方法是在声明变量的时候不赋初值,但是要在这个变量所在的类的所有的构造函数中对这个变量赋初值。
  • static和abstract能同时用吗

    不能,因为static方法独立于任何实例,因此static方法必须被实现,而不能是抽象的abstract。

  • 内部类可以调用外部的数据吗?如果是静态的呢?

    可以。静态内部类只能访问外部类的静态成员,否则编译会报错

  • 重写(override)和重载(overload)的区别

    **重载:**发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。

    **重写:**发生在父子类中,**方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;**如果父类方法访问修饰符为 private 则子类就不能重写该方法。

  • 返回值不同的重载,可以吗?为什么?

    不可以。在java语言中,要重载(overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,因此java语言里面无法仅仅依靠返回值不同来对一个已有的方法进行重载。

    • java代码层面的特征签名:方法名称+参数顺序+参数类型
    • 字节码文件的特征签名:以上+方法返回值+受查异常表
  • equals和==

    • ==: 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型比较的是值,引用数据类型比较的是内存地址)。

    • equals(): 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:

    • 情况1:类没有覆盖写equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。

    • 情况2:类override了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。

    • 举个例子:

      public class test1 {
          public static void main(String[] args) {
              String a = new String("ab"); // a 为一个引用
              String b = new String("ab"); // b为另一个引用,对象的内容一样
              String aa = "ab"; // 放在常量池中
              String bb = "ab"; // 从常量池中查找
              if (aa == bb) // true
                  System.out.println("aa==bb");
              if (a == b) // false,非同一对象
                  System.out.println("a==b");
              if (a.equals(b)) // true
                  System.out.println("aEQb");
              if (42 == 42.0) { // true
                  System.out.println("true");
              }
          }
      }
      

      说明:

    • String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。

    • 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

  • String和StringBuffer、StringBuilder

  1. 可变性:String 类中使用 final 关键字修饰字符数组来保存字符串private final char value[],所以 String 对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value但是没有用 final 关键字修饰,所以这两种对象都是可变的。
  2. **线程安全性:String 中的对象是不可变的,也就可以理解为常量,线程安全。**AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁(synchronized),所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
  3. **性能:**每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。单线程操作字符串缓冲区下操作大量数据: 适用StringBuilder;多线程操作字符串缓冲区下操作大量数据: 适用StringBuffer。
  • hashCode 与 equals (重要)

    • hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)。
    • 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相同的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用equals()方法来检查 hashcode 相等的对象如果equals方法返回为true,HashSet 就不会让其加入操作成功。如果返回false,就会重新散列到其他位置。
    • hashCode()与equals()的相关规定
    1. 如果不需要将该类的对象存放到哈希的集合中,比较对象相等时只需要重写equals()方法
    2. 如果需要将该类的对象存放到哈希的集合中,则需要重写hashCode()方法
    • 例子

    class Person {
    int age;
    String name;

     public Person(String name, int age) {
    	 this.name = name;
    	 this.age = age;
     }
    
     public String toString() {
    	 return name + " - " +age;
     }
    
     /** 
      * @desc重写hashCode 
      */  
     @Override
     public int hashCode(){  
    	 int nameHash =  name.toUpperCase().hashCode();
    	 return nameHash ^ age;
     }
    
     /** 
      * @desc 覆盖equals方法 
      */  
     @Override
     public boolean equals(Object obj){  
    	 if(obj == null){  
    		 return false;  
    	 }  
    	   
    	 //如果是同一个对象返回true,反之返回false  
    	 if(this == obj){  
    		 return true;  
    	 }  
    	   
    	 //判断是否类型相同  
    	 if(this.getClass() != obj.getClass()){  
    		 return false;  
    	 }  
    	   
    	 Person person = (Person)obj;  
    	 return name.equals(person.name) && age==person.age;  
     } 
    

    }

  • Object类有哪些方法

    • hashCode(),equals()
    • toString()
    • clone()
    • wait(),notify(),notifyAll()
    • finalize()
  • collection和collections

    • Collections是个java.util下的类,它包含有各种有关集合操作的静态方法。

    • Collection是个java.util下的接口,它是各种集合结构的父接口。

  • length,length()和size()

    • java 中的length 属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性.

    • java 中的length()方法是针对字符串String说的,如果想看这个字符串的长度则用到 length()这个方法.

    • .java 中的size()方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看!

  • ArrayList简介

ArrayList 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。**在添加大量元素前,应用程序可以使用ensureCapacity操作来增加 ArrayList 实例的容量。**这可以减少递增式再分配的数量。

它继承于AbstractList,实现了List,RandomAccess,Cloneable,java.io.Serializable这些接口。

在我们学数据结构的时候就知道了线性表的顺序存储,插入删除元素的时间复杂度为O(n),求表长以及增加元素,取第 i 元素的时间复杂度为O(1)

  • ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
  • ArrayList 实现了RandomAccess 接口, RandomAccess 是一个标志接口,表明实现这个这个接口的 List 集合是支持快速随机访问的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。
  • ArrayList 实现了Cloneable 接口,即覆盖了函数 clone(),能被克隆
  • ArrayList 实现java.io.Serializable 接口,这意味着ArrayList支持序列化能通过序列化去传输
  • 和 Vector 不同,ArrayList 中的操作不是线程安全的!所以,建议在单线程中才使用 ArrayList,而在多线程中可以选择 Vector 或者 CopyOnWriteArrayList。

LinkedList简介

LinkedList是一个实现了List接口和Deque接口的双端链表。 LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有队列的特性; LinkedList不是线程安全的,如果想使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法:

List list=Collections.synchronizedList(new LinkedList(...));

看LinkedList类中的一个内部私有类Node就很好理解了:

private static class Node<E> {
        Node<E> prev;//前驱节点
        E item;//节点值
        Node<E> next;//后继节点

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

这个类就代表双端链表的节点Node。这个类有三个属性,分别是前驱节点,本节点的值,后继结点。

获取尾节点(index=-1)数据方法:getLast()方法在链表为空时,会抛出NoSuchElementException,而peekLast()则不会,只是会返回null

**删除方法:removeLast()在链表为空时将抛出NoSuchElementException,而pollLast()**方法返回null

  1. **是否保证线程安全:**ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;

  2. **底层数据结构:**Arraylist 底层使用的是Object数组;LinkedList 底层使用的是双向链表数据结构;

  3. 插入和删除是否受元素位置的影响:ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。比如:执行add(E e)?方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element)?)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ②LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。

  4. **是否支持快速随机访问:**LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)?方法)。

  5. **内存空间占用:**ArrayList的空间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。

ArrayList扩容机制

/**
     * 如有必要,增加此ArrayList实例的容量,以确保它至少能容纳元素的数量
     * @param   minCapacity   所需的最小容量
     */
    public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table
            ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size.
            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }
   //得到最小扩容量
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
              // 获取默认的容量和传入参数的较大值
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }
  //判断是否需要扩容,上面两个方法都要调用
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // 如果说minCapacity也就是所需的最小容量大于保存ArrayList数据的数组的长度的话,就需要调用grow(minCapacity)方法扩容。
        //这个minCapacity到底为多少呢?举个例子在添加元素(add)方法中这个minCapacity的大小就为现在数组的长度加1
        if (minCapacity - elementData.length > 0)
            //调用grow方法进行扩容,调用此方法代表已经开始扩容了
            grow(minCapacity);
    }

/**
 * 允许分配的数组最大容量
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
 * ArrayList扩容的核心方法
 * @param minCapacity:最小扩容量
 */
private void grow(int minCapacity) {
        // oldCapacity为旧容量=数组当前长度
        int oldCapacity = elementData.length;

        // newCapacity为新容量=oldCapacity的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);

        // 然后检查新容量是否大于最小需要容量,若还是小于最小扩容量,那么就把最小扩容量当作数组的新容量
        if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;

        // 再检查新容量是否超出了ArrayList所定义的最大容量,
        // 若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE,
        // 如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。
        if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);

        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
        }

private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
        }
  • ArrayList和Vector

    • Vector与ArrayList一样,也是通过数组实现的,Vector类的所有方法都是同步的。它也是线程安全的,而Arraylist是线程异步(ASynchronized)的,是不安全的。如果不考虑到线程的安全因素,一般用Arraylist效率比较高。

    • 使用ArrayList时,如果不指定大小,会生成一个空的数组;

      使用Vector时,如果不指定大小,会默认生成一个10个元素大小的数组

    • Vector 实现类中有一个变量 capacityIncrement 用来表示每次容量自增时应该增加多少,如果不指定,默认为0

      在扩容时,会判断,如果指定了capacityIncrement,会先把数组容量扩大到oldCapacity + capacityIncrement,如果没有指定capacityIncrement,会先把数组容量扩大到2倍的oldCapacity, 然后再进行判断扩充后的容量是否满足要求,如果不满足要求,直接将容量扩大到指定大小,源码如下:

      private void grow(int minCapacity) {
              // overflow-conscious code
              int oldCapacity = elementData.length;
              int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                               capacityIncrement : oldCapacity);
              if (newCapacity - minCapacity < 0)
                  newCapacity = minCapacity;
              if (newCapacity - MAX_ARRAY_SIZE > 0)
                  newCapacity = hugeCapacity(minCapacity);
              elementData = Arrays.copyOf(elementData, newCapacity);
          }
      
  • Set不能存放重复元素,其底层是如何实现的

    • HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了clone()?writeObject()readObject()是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。
    • 当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。
  • HashMap原理

    • JDK1.8 之前 HashMap 底层是数组和链表结合在一起使用也就是链表散列HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过(n - 1) & hash判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。

    • JDK 1.8 HashMap 的 hash 方法源码:

      static final int hash(Object key) {
            int h;
            // key.hashCode():返回散列值也就是hashcode
            // ^ :按位异或
            // >>>:无符号右移,忽略符号位,空位都以0补齐
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//hashcode的高16位与低16位异或
        }
      
    • 所谓**“拉链法”**就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可:

jdk1.8之前的内部结构

  • 为什么HashMap无序?为什么不安全?

    因为元素在底层数组中的索引位置是经过hash算法(key的哈希值+扰动)计算出来的,与原来的顺序没有关系。线程不安全,多线程访问的情况下,会出现问题。

  • Hash碰撞的解决方法

    • 数组+链表
    • 当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。

jdk1.8之后的内部结构

  • HashMap满了之后怎么扩容(rehash)

    • loadFactor负载因子/装填因子

      loadFactor加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。

      loadFactor太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为0.75f是官方给出的一个比较好的临界值

      给定的默认容量为 16,负载

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值