题目

文章目录

1. Hashtable、HashMap 和 ConcurrentHashMap的区别?

Hashtable、HashMap 和 ConcurrentHashMap的区别
Hashtable

  • 底层:数组 + 链表,无论 key 还是 value 都不能为 null,线程安全。实现线程安全的方式是在修改数据时,锁住整个 Hashtable,但是效率低。ConcurrentHashMap 做了优化
  • 初始 size 为 11,扩容:newsize = oldsize * 2 + 1
  • 计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length

HashMap

  • 数组+链表实现,可以存储null键和null值,线程不安全
  • 初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
  • 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
  • 当Map中元素总数超过Entry数组的75%,触发扩容操作,这是为了减少链表长度,元素分配更均匀
  • 计算index方法:index = hash & (tab.length – 1)

负载极限

  1. 负载极限是 0 ~ 1 的数值,它决定了 hash 表最大的填满程度。当hash表中的负载因子达到指定的“负载极限”时,hash表会自动成倍地增加容量(桶的数量),并将原有的对象重新分配,放入新的桶内,这称为rehashing。
  2. HashMap和Hashtable的构造器允许指定一个负载极限,HashMap和Hashtable默认的“负载极限”为0.75,这表明当该hash表的3/4已经被填满时,hash表会发生rehashing
  3. “负载极限”的默认值(0.75)是时间和空间成本上的一种折中:
    • 较高的“负载极限”可以降低hash表所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的操作(HashMap的get()与put()方法都要用到查询)。例如,负载极限较高,说明当桶被填的比较满的时候才会扩容,也就是说这是可能已经发生了很多次的冲突,每个链表的长度是比较长的,所以会增加查询的时间开销。
    • 较低的“负载极限”会提高查询数据的性能,但会增加hash表所占用的内存开销
      程序猿可以根据实际情况来调整“负载极限”值。负载极限较低,说明桶还有很多没有使用的时候就开始扩容,扩容时原有对象重新分配,因为此时桶的数量比较高,所以每条链表的长度较短,这是会提高查询的性能。

ConcurrentHashMap

  • 分段的数组+链表实现,线程安全
  • Hashtable 的 synchronized 是针对整张 Hash 表的,即每次锁住整张表让线程独占,ConcurrentHashMap 允许多个修改操作并发进行,其关键在于使用了锁分离技术
  • 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
  • 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容

2. HashMap内部具体如何实现的?

HashMap内部实现
在这里插入图片描述
基本成员属性

//默认的容量,即默认的数组长度 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大的容量,即数组可定义的最大长度 
static final int MAXIMUM_CAPACITY = 1 << 30;
transient Node<K,V>[] table;

这就是上述提到的数组,数组的元素都是 Node 类型,数组中的每个 Node 元素都是一个链表的头结点,通过它可以访问连接在其后面的所有结点。其实你也应该发现,上述的容量指的就是这个数组的长度。

//实际存储的键值对个数
transient int size;
//用于迭代防止结构性破坏的标量
transient int modCount;
int threshold;
final float loadFactor;
//HashMap 中默认负载因子为 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;

上面这三个属性是相关的,threshold 代表的是一个阈值,通常小于数组的实际长度。伴随着元素不断的被添加进数组,一旦数组中的元素数量达到这个阈值,那么表明数组应该被扩容而不应该继续任由元素加入。而这个阈值的具体值则由负载因子(loadFactor)和数组容量来决定,公式:threshold = capacity * loadFactor

3. 如果hashMap的key是一个自定义的类,怎么办?

  1. 自定义类中的 hashCode() 方法继承与 Object 类,其 hashCode 码默认为内存地址,这样即便有相同含义的两个对象,比较也是不相同的,equals() 方法用于比较内存地址是否相等。

  2. 因为如果两个对象相等,它们的hashcode一定相同;而对象相等是通过 equals() 推出的。例如,如果重写了 equals() 令两个原本 不相同的对象 A 和 B 在新的equals()的规则下是相同的对象,那么就要重写二者的hashCode()使得二者的hashcode一致。

  3. Student s1 = new Student(“wei”,“man”);
    Student s2 = new Student(“wei”,“man”);
    我们可以拿这两行代码作为例子来解释。我们在 HashMap 中首先存入了 s1 对象的键值对,这时我们要存入 s2 对象的键值对,如果我们不重写 hashcode() 和 equals() 方法,那么,它们就会被认为是两个不同的键,但是在实际中,我们认为它们俩是相同的。所以我们需要重写这两个方法。所以我们重写 equals() 令两个原本 不相同的对象 s1 和 s2 在新的equals()的规则下是相同的对象,那么就要重写二者的 hashCode() 使得二者的 hashcode 一致。

  4. hashcode 不是完全可靠的,有时不同的对象生成的 hashcode 的值也是相同的。

    1. equal()相等的两个对象他们的hashCode()肯定相等,也就是用equal()对比是绝对可靠的
    2. hashCode()相等的两 个对象他们的equal()不一定相等,也就是hashCode()不是绝对可靠的

4. 介绍一下Syncronized锁。如果用这个关键字修饰一个静态方法,锁住了什么?如果修饰成员方法,锁住了什么?

非线程安全其实会在多个线程对同一个对象中的实例变量进行并发访问时发生。Java 中的 Synchronized 锁可以在多线程环境下用来作为线程安全的同步锁。synchronized 是 Java 语言的关键字,当它用来修饰一个方法或者代码块的时候,能够保证在同一时刻最多只有一个线程在执行该段代码。

synchronized 有多个叫法,而每个叫法都表明synchronized 的特性:

  1. 内置锁:内置锁(又叫 隐式锁):synchronized 是内置于JDK中的,底层实现是native;同时,加锁、解锁都是JDK自动完成,不需要用户显式地控制,非常方便。
  2. 同步锁:synchronized 用于同步线程,使线程互斥地访问某段代码块、方法。这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。
  3. 对象锁:准确来说,是分为对象锁、类锁。synchronized 以当前的某个对象为锁,线程必须通过互斥竞争拿到这把对象锁,从而才能访问 临界区的代码,访问结束后,就会释放锁,下一个线程才有可能获取锁来访问临界区(被锁住的代码区域)。synchronized锁 根据锁的范围分为 对象锁 和 类锁。对象锁,是以对象实例为锁,当多个线程共享访问这个对象实例下的临界区,只需要竞争这个对象实例便可,不同对象实例下的临界区是不用互斥访问;而类锁,则是以类的class对象为锁,这个锁下的临界区,所有线程都必须互斥访问,尽管是使用了不同的对象实例。
  4. 总的来说,对象锁的粒度要比类锁的粒度要细,引起线程竞争锁的情况比类锁要少的多,所以尽量别用类锁,锁的粒度越少越好。

如果修饰成员方法,锁住的是对象,下面是验证及解释:

/*
* 从执行的代码中我们可以看出,doLongTimeTask方法没有执行完毕线程B就执行了
* otherMethod方法。这是为什么呢?这是因为synchronized(this)代码块锁定的
* 是当前对象,线程A执行的时候获取得到对象锁this,然后执行其中的代码。
* 为什么线程A没有执行完线程B就执行了呢?这是因为线程B要执行otherMethod方法
* 是不需要获取对象锁的,所以只要线程B的到CPU时间,就可以执行otherMethod方法了。
* */
public class Task {
    public void otherMethod(){
        System.out.println("----------------------run otherMethod");
    }
    public void doLongTimeTask(){
        synchronized (this){
            for (int i = 0; i < 1000000; i++) {
                System.out.println("synchronized threadName = " + Thread.currentThread().getName() + " i " + (i+1));

            }
        }
    }
}

public class MyThread1 extends Thread {
    private Task task;

    public MyThread1(Task task) {
        super();
        this.task = task;
    }

    public void run(){
        super.run();
        task.doLongTimeTask();
    }
}

public class MyThread2 extends Thread {
    private Task task;

    public MyThread2(Task task) {
        super();
        this.task = task;
    }

    public void run(){
        super.run();
        task.otherMethod();
    }
}

public class Run {
    public static void main(String[] args) {
        try {
            Task task = new Task();
            MyThread1 thread1 = new MyThread1(task);
            thread1.start();
            Thread.sleep(100);
            MyThread2 thread2 = new MyThread2(task);
            thread2.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

这就充分说明了synchronized(this)代码块锁定的是对象,而不是代码。

如果这个关键字修饰了一个静态方法,那就是对当前的*.java文件对应的Class类进行持锁。也就是说锁住的是一个类。

5. 多线程中的i++线程安全吗?为什么?

i++ 的操作要分为三步:

  1. 取得原有的 i 值
  2. 计算 i - 1
  3. 对 i 进行赋值
    如果多个线程同时访问,那么一定会出现非线程安全问题
    i++问题

6. ArrayList和LinkedList的区别,如果一直在list的尾部添加元素,用哪个效率高?

ArrayList和LinkedList的区别

  1. ArrayList 底层基于动态数组,当 ArrayList 中存储的元素超过其最大容量,ArrayList 可以进行扩容。若使用默认构造函数,则ArrayList的默认容量大小是10。当ArrayList容量不足以容纳全部元素时,ArrayList会重新设置容量:新的容量=“(原始容量x3)/2 + 1”。
  2. LinkedList 基于链表的动态数组,数据添加删除效率高,只需要改变指着即可,但是访问数据平均效率低,需要对链表进行遍历。
  3. ArrayList和LinkedList可想从名字分析,它们一个是Array(动态数组)的数据结构,一个是Link(链表)的数据结构,此外,它们两个都是对List接口的实现。
    前者是数组队列,相当于动态数组;后者为双向链表结构,也可当作堆栈、队列、双端队列
  4. 当随机访问List时(get和set操作),ArrayList比LinkedList的效率更高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。
  5. 当对数据进行增加和删除的操作时(add和remove操作),LinkedList比ArrayList的效率更高,因为ArrayList是数组,所以在其中进行增删操作时,会对操作点之后所有数据的下标索引造成影响,需要进行数据的移动。
  6. ArrayList主要控件开销在于需要在lList列表预留一定空间;而LinkList主要控件开销在于需要存储结点信息以及结点指针信息。

如果一直在 list 的尾部添加数据,那个效率更高呢?
当输入的数据一直是小于千万级别的时候,大部分是Linked效率高。原LinkedList每次增加的时候,会new 一个Node对象来存新增加的元素,所以当数据量小的时候,这个时间并不明显,而ArrayList需要扩容,所以LinkedList的效率就会比较高,其中如果ArrayList出现不需要扩容的时候,那么ArrayList的效率应该是比LinkedList高的,当数据量很大的时候,new对象的时间大于扩容的时间,那么就会出现ArrayList’的效率比Linkedlist高了。

7. 介绍一下volatile

介绍一下volatile
你真的了解volatile关键字吗?

  1. 关键字 volatile 的作用是使变量在多个线程间可见。volatile 关键字的作用是强制从公共堆栈中取得变量的值,而不是从线程私有数据中取得变量的值。
  2. 使用 volatile 关键字增加了实例变量在多个线程之间的可见性。但 volatile 最为致命的缺点是不支持原子性。
  3. 下面将 synchronized 和 volatile 进行以下比较:
    1. 关键字 volatile 是线程同步的轻量级实现,所以 volatile 性能肯定比 synchronized 要好, 并且 volatile 只能修饰变量,而 synchronized 可以修饰方法,以及代码块。
    2. 多线程访问 volatile 不会发生阻塞,而 synchronized 会发生阻塞。
      volatile 能保证数据的可见性,但不能保证原子性;而 synchronized 可以保证原子性。
    3. 关键字 volatile 解决的是变量在多个线程之间的可见性;而 synchronized 解决的是多个线程之间访问资源的同步性。
    4. 线程安全包含原子性和可见性两个方面,Java 的同步机制都是围绕这两个方面来确保线程安全的。

8. 如何设计一个线程安全的计数器?

线程安全的计数器实现原理简介:在java中volatile关键字可以保证共享数据的可见性,它会把更新后的数据从工作内存刷新进共享内存,并使其他线程中工作内存中的数据失效,进而从主存中读入最新值来保证共享数据的可见性。

import java.util.concurrent.atomic.AtomicInteger;

/*
* 实现count的计数,两个线程计数跑到1000
* */
public class SafeCalc {
    private static AtomicInteger count = new AtomicInteger(0);
    //volatile int count = 0;
    synchronized public void countNum(){
        while(count.get() < 100){
            count.incrementAndGet();
            System.out.println("线程" + Thread.currentThread().getName()+": " + count);
        }
    }
}

public class ThreadA extends Thread {
    private SafeCalc safeCalc;

    public ThreadA(SafeCalc safeCalc) {
        super();
        this.safeCalc = safeCalc;
    }

    public void run(){
        while(true){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            safeCalc.countNum();
        }
    }
}

public class ThreadB extends Thread {
    private SafeCalc safeCalc;

    public ThreadB(SafeCalc safeCalc) {
        super();
        this.safeCalc = safeCalc;
    }

    public void run(){
        while(true){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            safeCalc.countNum();
        }
    }
}

public class Run {
    public static void main(String[] args) {
        SafeCalc safeCalc = new SafeCalc();
        ThreadA a = new ThreadA(safeCalc);
        ThreadB b = new ThreadB(safeCalc);
        a.setName("A");
        b.setName("B");
        a.start();
        b.start();
    }
}

9. 讲一下浏览器从接收到一个URL到最后展示出页面,经历了哪些过程?

过程

  1. 首先在浏览器地址栏中输入 url
  2. 浏览器先查看浏览器缓存-系统缓存-路由器缓存,如果有缓存,会直接在屏幕上显示页面的内容,若没有,跳到第三步。
  3. 在发送http请求前,需要域名解析(DNS解析),解析获取相应的IP地址。
  4. 浏览器向服务器发起tcp连接,与浏览器建立tcp三次握手。
  5. 握手成功后,浏览器向服务器发送http请求,请求数据包。
  6. 服务器处理收到的请求,将数据返回至浏览器。
  7. 浏览器收到HTTP响应。
  8. 读取页面内容,浏览器渲染,解析html源码。
  9. 生成Dom树、解析css样式、js交互。
  10. 客户端和服务器交互。
  11. ajax查询。
    客户端:
  12. (应用层开始)获取URL,通过负责域名解析的DNS服务获取网址的IP地址,根据HTT协议生成HTTP请求报文(应用层结束)。
  13. (传输层)根据TCP协议连接从客户端到服务端(通过三次握手),客户端给服务端发送一个带SYN(同步)标志的数据包,然后服务端接收到信息再给客户端回传一个带有SYN/ACK标志的以示传达确认信息,客户端最后再传送一个带ACK标志的数据包代表握手结束,连接成功。TCP协议再把请求报文段按序号分割成多个报文段(传输层结束)
  14. (网络层开始)根据IP协议(传输数据),ARP协议(获取MAC地址),OSPF协议(选择最优路径),搜索服务器地址,一边中转一边传输数据(网络层结束)
  15. (数据链路层开始)到达后通过数据链路层,物理层负责0,1比特流与物理设备电压高低,光的闪灭之间的互换。数据链路层负责将0,1序列划分为数据帧从一个节点传输到临近的另一个节点,这些节点是通过MAC来唯一标识的(MAC,物理地址,一个中主机会有一个MAC地址)。 (数据链路层结束)

服务端
通过数据链路层 - >通过网络层 - >再通过传输层(根据TCP协议接收请求报文并重组报文段) - >再通过应用层(通过HTTP协议对请求的内容进行处理) - >再通过应用层 - >传输层 - >网络层 - >数据链路层 - >到达客户端

客户端
通过数据链路层 - >网络层 - >传输层(根据TCP协议接收响应报文并重组) - >应用层(HTTP协议对响应进行处理) - >浏览器渲染页面 - >断开连接协议四次挥手)

四次挥手
主动方发送标志位:(ACK + FIN)+(发送序号= 200 +确认序号= 500)第一次挥手
被动方接收后发送标志位:ACK +(发送序号=主动方确认序号500 +确认序号=主动方发送序号+1201)第二次挥手
标志位:(ACK + FIN)+(发送序号=主动方确认序号+1 501)第三次挥手
主动方接收后发送标志位:(ACK)+(发送序号=被动方的确认序号201 +确认序号=被动方的发生序号+1502)

10. 写SQL:找出每个城市的最新一条记录

id 城市 人口 信息 创建时间
1 北京 100 info1 时间戳
2 北京 100 info2 时间戳
3 上海 100 info3 时间戳
4 上海 100 info4 时间戳

SELECT id, 城市, 人口, 信息, MAX(创建时间) FROM table GROUP BY 城市

11. 写一个函数,找到一个文件夹下所有文件,包括子文件夹

12. 数据库索引介绍一下

数据库索引1
数据库索引2

  • 背景知识
  • 数据库基本操作的实现
  • 索引的产生
    • Dense Index(稠密索引)
      根据减少无效数据访问的原则,我们将键的值拿过来存放到独立的块中。并且为 每一个键值添 加一个指针, 指向原来的数据块。这就是‘索引’的祖先Dense Index。当进行定位操作时,不再进行表扫描。而是进行索引扫描(Index Scan),依次读出所有的索引块,进行键值的匹配。当找到匹配的键值后,根据该行的指针直接读取对应的数据块,进行操作。
  • 索引的进化
    • 折半块查找

    • Parse Index(稀疏索引)
      在稀疏索引中,只为索引码的某些值建立索引项。同理因为稀疏索引也是聚集索引。每一个索引项包括索引值以及指向该搜索码值的第一条数据记录的指针。在这里插入图片描述

  • 索引的数据结构
    索引的数据结构
    • 二分查找
    • 二叉树查找
    • B- 树和 B+树
      在 B+ 树中,所有记录节点都是按键值的大小顺序存放在同一层叶子节点,由各叶子节点指针进行连接。

聚集索引和非聚集索引的区别

聚集索引和非聚集索引的根本区别是表记录的排列顺序与索引的排列顺序是否一致。

  1. 聚集索引表记录的排列顺序与索引的排列顺序一致

    • 优点是查询速度快,因为一旦具有第一个索引值的纪录被找到,具有连续索引值的记录也一定物理的紧跟其后。
    • 缺点是对表进行修改速度较慢,这是为了保持表中的记录的物理顺序与索引的顺序一致,而把记录插入到数据页的相应位置,必须在数据页中进行数据重排, 降低了执行速度。
    • 建议使用聚集索引的场合为:
      a. 此列包含有限数目的不同值;
      b. 查询的结果返回一个区间的值;
      c. 查询的结果返回某值相同的大量结果集。
  2. 非聚集索引指定了表中记录的逻辑顺序,但记录的物理顺序和索引的顺序不一致。聚集索引和非聚集索引都采用了B+树的结构,但非聚集索引的叶子层并不与实际的数据页相重叠,而采用叶子层包含一个指向表中的记录在数据页中的指针的方式。

  3. 聚集索引确定表中数据的物理顺序。

  4. 非聚集索引中,数据存储在一个地方,索引存储在另一个地方,索引带有指针指向数据的存储位置。

  5. 聚集索引的顺序就是数据的物理存储顺序,而非聚集索引的顺序和数据物理排列无关。因为数据在物理存放时只能有一种排列方式,所以一个表只能有一个聚集索引。在SQL SERVER中,索引是通过二叉树的数据结构来描述的;我们可以如此理解这个两种索引:聚集索引的叶节点就是数据节点,而非聚集索引的叶节点仍然是索引节点,只不过其包含一个指向对应数据块的指针。

13. 数据库事务的四大特性(ACID)

  1. 原子性
    原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚。
  2. 一致性
    一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
    拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
  3. 隔离性
    隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
    即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。
  4. 持久性
    持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
    例如我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务以及正确提交,即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成,否则就会造成我们看到提示事务处理完毕,但是数据库因为故障而没有执行事务的重大错误。

14. 不考虑事务的隔离性会发生的问题

  1. 脏读
    脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。

当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。例如:用户A向用户B转账100元,对应SQL命令如下:

update account set money=money+100 where name=’B’;  (此时A通知B)
update account set money=money - 100 where name=’A’;

当只执行第一条SQL时,A通知B查看账户,B发现确实钱已到账(此时即发生了脏读),而之后无论第二条SQL是否执行,只要该事务不提交,则所有操作都将回滚,那么当B以后再次查看账户时就会发现钱其实并没有转。

  1. 不可重复读
    不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。

例如事务T1在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据就得到了不同的结果,发送了不可重复读。

不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。

在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。但在另一些情况下就有可能发生问题,例如对于同一个数据A和B依次查询就可能不同,A和B就可能打起来了……

  1. 虚读(幻读)
    幻读是事务非独立执行时发生的一种现象。例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。

幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。

15. 数据库的隔离级别

  1. Read uncommitted(读未提交):最低级别,任何情况都无法保证。
  2. Read committed(读已提交):可避免脏读的发生。
  3. Repeatable read(可重复度):可避免脏读、不可重复读的发生。
  4. Serializable (串行化):可避免脏读、不可重复读、幻读的发生。

以上四种隔离级别最高的是Serializable级别,最低的是Read uncommitted级别,当然级别越高,执行效率就越低。像Serializable这样的级别,就是以锁表的方式(类似于Java多线程中的锁)使得其他的线程只能在锁外等待,所以平时选用何种隔离级别应该根据实际情况。在MySQL数据库中默认的隔离级别为Repeatable read (可重复读)

16. MyISAM和Innodb区别在哪 ?

17. 哈希索引与B+树索引的区别,及适用场景

18. springIOC原理?自己实现IOC要怎么做,哪些步骤?

工厂设计模式
IOC实现原理

对于使用反射机制实现的工厂模式,相比传统的工厂设计模式,当增加新类型的时候,只需要将类名传入工厂即可获取对应类型的对象。使得代码变得更加灵活,然而用户无法准确传入完整的包名和类型,因此我们对于这个工厂方法进行了进一步的改造。形成了Ioc思想的雏形。

19. Spring AOP 的原理是什么?

SpringAOP原理

  • 什么是SpringAOP ?
    Spring框架的AOP机制可以让开发者把业务流程中的通用功能抽取出来,单独编写功能代码。在业务流程执行过程中,Spring框架会根据业务流程要求,自动把独立编写的功能代码切入到流程的合适位置。

20. 动态代理有哪些实现方式?

代理模式是Java常见的设计模式之一。所谓代理模式是指客户端并不直接调用实际的对象,而是通过调用代理,来间接的调用实际的对象。
为什么要采用这种间接的形式来调用对象呢?一般是因为客户端不想直接访问实际的对象,或者访问实际的对象存在困难,因此通过一个代理对象来完成间接的访问。
在现实生活中,这种情形非常的常见,比如请一个律师代理来打官司。

/*
* 静态代理:
* 所谓代理模式是指客户端并不直接调用实际的对象,而是通过调用代理,来间接的调用实际的对象
* 在这里,我们定义了两个委托类(实现类),RealSubject1和RealSubject2
* Client客户端想要访问特定的委托类的时候,不能直接访问,而是要通过代理类来访问
* 缺点:
* 1.代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法。这样就出现了大量的代码重复。
* 如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。
* 增加了代码维护的复杂度。
* 2.静态代理类只能为特定的接口服务,即一个接口需要一个代理类,如果代理类想要为多个接口服务则需要建立很多
* 个代理类。这个代理只能为接口Subject服务,如果再增加一个别的接口如Function接口,就需要我们再次添加代理Function的代理类。
* */
package proxy.static_proxy;

public interface Subject {
    void visit();
    void print();
}

package proxy.static_proxy;

public class RealSubject1 implements Subject{
    private String name = "Bob";
    @Override
    public void visit() {
        System.out.println(name);
    }

    @Override
    public void print() {

    }
}

package proxy.static_proxy;

public class RealSubject2 implements Subject{
    private String name = "Steven";
    public void visit() {
            System.out.println(name);
        }

    @Override
    public void print() {

    }

}

public class ProxySubject implements Subject{
    private Subject subject;

    public ProxySubject(Subject subject) {
        this.subject = subject;
    }

    @Override
    public void visit() {
        subject.visit();
    }

    @Override
    public void print() {

    }
}

package proxy.static_proxy;

public class Client {
    public static void main(String[] args) {
        ProxySubject subject = new ProxySubject(new RealSubject2());
        subject.visit();
    }
}

静态代理动态代理
动态代理

  • 动态代理
    根据上面的学习,我们发现每一个代理只能为一个接口服务,这样在程序开发的过程中必然会产生许多的代理类,所以我们就会想办法可以通过一个代理类完成全部的代理功能,那么我们就需要用动态代理。

在上面的示例中,一个代理只能代理一种类型,而且在编译阶段就已经确定了代理对象。而动态代理在运行时,通过反射机制实现动态代理,并且能够代理各种类型的对象。

在Java中要想实现动态代理机制,需要java.lang.reflect.InvocationHandler接口和 java.lang.reflect.Proxy 类的支持。

java.lang.reflect.InvocationHandler 接口定义如下:

//Object proxy:被代理的对象  
//Method method:要调用的方法  
//Object[] args:方法调用时所需要参数  
public interface InvovationHandler{
   public Object invoke(Object proxt, Method method, Object[] args) throws Throwable;
}

java.lang.reflect.Proxy类的定义如下:

//CLassLoader loader:类的加载器  
//Class<?> interfaces:得到全部的接口  
//InvocationHandler h:得到InvocationHandler接口的子类的实例  
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException  
//动态代理类只能代理接口(不支持抽象类),代理类都需要实现InvocationHandler类,实现invoke方法。该invoke方法就是调用被代理接口的所有方法时需要调用的,该invoke方法返回的值是被代理接口的一个实现类 

21. SpringMVC 的执行流程

在这里插入图片描述

  1. 处理器指的就是我们写的 Controller 。用户的请求是怎么到达正确的 Controller 的呢?
  2. 比如用户输入一个网址 hocalhost:8080/springmvc/item/itemlist.action 发送请求,这时用户的请求到达前端控制器 DispatcherServlet,前端控制器会将 /item/itemlist.action 取出来,执行请求查询Handler(Controller) ,就是将这个地址交给处理器映射器(HandlerMapping),处理器映射器会查找 Controller,返回的是包名+类名+方法名,之后将这个返回值返回给前端控制器。
  3. 找到要具体执行的方法以后,请求执行 Handler(Controller) ,这是用到处理器适配器,这个方法是由处理器适配器来执行的。处理器Handler返回一个ModelAndView给处理器适配器,然后处理器适配器将ModelView交给前端控制器。
  4. 前端控制器拿到ModelView以后将它交给视图解析器ViewResolver,视图解析器会将ModelAndView中的数据加入到jsp页面中,形成一个View对象(就是有数据也有页面的一个对象)。最后经过渲染视图,将页面返回给客户。

注解映射器和适配器

注解式映射器,对类中标记了 @RequestMapping 的方法进行映射。根据@ResquestMapping定义的url匹配 @ResquestMapping 标记的方法,匹配成功返回HandlerMethod对象给前端控制器。HandlerMethod 对象中封装url对应的方法Method。

@RequestMapping:定义请求url到处理器功能方法的映射

22. Java 中的队列有哪些?

首先,我们要明白使用队列的目的是什么?如果是一些及时的消息的处理,并且处理时间很短的情况下是不需要使用队列的,直接阻塞式的方法调用就可以了。但是,如果在消息处理的时候特别费时间,这个时候如果有新的消息来了,就只能处于阻塞状态,造成用户等待,这个时候在项目中引入队列是十分有必要的。当我们接受到消息后,先把消息放到队列中,然后再用新的线程进行处理,这个时候就不会有消息的阻塞了。
在这里插入图片描述

  1. Java 中的队列

    • 非阻塞队列
      • LinkedList
      • PriorityQueue
      • ConcurrentLinkedQueue
    • 阻塞队列
      • ArrayBlockingQueue
      • LinkedBlockingQueue
      • PriorityBlockingQueue
  2. 什么是阻塞队列?
    阻塞队列
    阻塞队列是一个在队列基础上又支持了两个附加操作的队列。
    2个附加操作:
    支持阻塞的插入方法:队列满时,队列会阻塞插入元素的线程,直到队列不满。
    支持阻塞的移除方法:队列空时,获取元素的线程会等待队列变为非空。

  3. 阻塞队列的应用场景
    阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。简而言之,阻塞队列是生产者用来存放元素、消费者获取元素的容器。

  4. BlockingQueue

    1. 放入数据
      (1)offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.(本方法不阻塞当前执行方法的线程);
      (2)offer(E o, long timeout, TimeUnit unit):可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。
      (3)put(anObject):把 anObject 加到 BlockingQueue 里,如果 BlockQueue没有空间,则调用此方法的线程被阻断直到 BlockingQueue 里面有空间再继续。
    2. 获取数据
      (1)poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null;
      (2)poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则知道时间超时还没有数据可取,返回失败。
      (3)take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入;
      (4)drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。
  5. 常见的 BlockingQueue
    在这里插入图片描述

    1. ArrayBlockingQueue
      ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同LinkedBlockingQueue。

    2. LinkedBlockingQueue
      而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

    3. PriorityBlockingQueue
      基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。

23. 数据结构中的队列?

队列是一种先入先出的线性表。队列只在线性表的两端进行操作,插入元素的一端称为表尾,删除元素的一端称为表头。

  • 循环队列
    队空:队头指针在队尾指针的下一位置时,队空: Q.front == Q.rear;
    队满:(Q.rear+1)%MAXSIZE=Q.front ,
    队满:当队头和队尾指针在同一位置时;
    队列长度:(Q.rear - Q.front + MAXSIZE) % MAXSIZE

24. Java 内存模型,方法区可以去掉吗?

在这里插入图片描述

  1. PC寄存器 / 程序计数器
    用来保存当前正在执行的程序的内存地址,所以程序执行的轨迹不可能一直都是线性执行。当有多个线程交叉执行时,被中断的线程的程序当前执行到哪条内存地址必然要保存下来,以便用于被中断的线程恢复执行时再按照被中断时的指令地址继续执行下去。为了线程切换后能恢复到正确的执行位置,每个线程都需要一个独立的程序计数器,各个线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存,这在某种程度上有点类似于“ThreadLocal”,是线程安全的。

  2. Java 栈(Java stack)
    Java栈总是与线程关联在一起的,每当创建一个线程,JVM就会为该线程创建对应的Java栈,在这个Java栈中又会包含多个栈帧(Stack Frame),这些栈帧是与每个方法关联起来的,每运行一个方法就创建一个栈帧,栈帧中存储着方法中定义的变量,如果是基本数据类型,就在栈中进行值的存储;如果是引用数据类型,存储的就是指向对象的地址。
    由于Java栈是与线程对应起来的,Java栈数据不是线程共有的,所以不需要关心其数据一致性,也不会存在同步锁的问题。
    在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。在Hot Spot虚拟机中,可以使用-Xss参数来设置栈的大小。栈的大小直接决定了函数调用的可达深度。

  3. 堆(Heap)
    **堆是JVM所管理的内存中国最大的一块,是被所有Java线程所共享的,不是线程安全的,在JVM启动时创建。**堆是存储Java对象的地方,这一点Java虚拟机规范中描述是:所有的对象实例以及数组都要在堆上分配。Java堆是GC管理的主要区域,从内存回收的角度来看,由于现在GC基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代再细致一点有Eden空间、From Survivor空间、To Survivor空间等。

  4. 方法区(Method area)
    在这里插入图片描述
    在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。

在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。

在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。

方法区存放了要加载的类的信息(名称、修饰符等)、类中的静态常量、类中定义为final类型的常量、类中的Field信息、类中的方法信息。当在程序中通过Class对象的getName.isInterface等方法来获取信息时,这些数据都来源于方法区。方法区是被Java线程锁共享的,不像Java堆中其他部分一样会频繁被GC回收,它存储的信息相对比较稳定,在一定条件下会被GC,当方法区要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。方法区也是堆中的一部分,就是我们通常所说的Java堆中的永久区 Permanet Generation,大小可以通过参数来设置,可以通过-XX:PermSize指定初始值,-XX:MaxPermSize指定最大值。

  1. 常量池(Constant pool)
    常量池
    常量池本身是方法区中的一个数据结构。常量池中存储了如字符串、final变量值、类名和方法名常量。常量池在编译期间就被确定,并保存在已编译的.class文件中。一般分为两类:字面量和应用量。字面量就是字符串、final变量等。类名和方法名属于引用量。引用量最常见的是在调用方法的时候,根据方法名找到方法的引用,并以此定为到函数体进行函数代码的执行。引用量包含:类和接口的权限定名、字段的名称和描述符,方法的名称和描述符。

  2. 本地方法栈(Native method stack)

Java 内存模型是围绕并发编程中原子性、可见性与有序性这三个特征建立的:
8. 原子性
9. 可见性
10. 有序性

25. 详细讲一下 Java 的 GC

26. JVM 的垃圾回收器,G1回收器跟其他回收器相比最大的区别在哪儿?

27. InnoDB 存储引擎是什么数据结构?怎么建立索引?索引和数据的关系?选主键的时候注意什么?如果选字符串形式的IP地址作为主键,会有什么问题?

B+树索引

TCP/IP协议

TCP/IP

  1. 数据链路层

  2. 网络层
    网络层是整个TCP/IP协议栈的核心,它的功能是把分组发往目标网络或主机。同时,为了尽快地发送分组,可能需要沿不同的路径同时进行分组传递。因此,分组到达的顺序和发送的顺序可能不同,这就需要上层必须对分组进行排序。
    网络互连层定义了分组格式和协议,即IP协议(Internet Protocol)。
    网络互连层除了需要完成路由的功能外,也可以完成将不同类型的网络(异构网)互连的任务。除此之外,网络互连层还需要完成拥塞控制的功能。

  3. 传输层
    在TCP/IP模型中,传输层的功能是使源端主机和目标端主机上的对等实体可以进行会话。在传输层定义了两种服务质量不同的协议。即:传输控制协议TCP和用户数据报协议UDP。 
    TCP协议是一个面向连接的、可靠的协议。它将一台主机发出的字节流无差错地发往互联网上的其他主机。在发送端,它负责把上层传送下来的字节流分成报文段并传递给下层。
    在接收端,它负责把收到的报文进行重组后递交给上层。TCP协议还要处理端到端的流量控制,以避免缓慢接收的接收方没有足够的缓冲区接收发送方发送的大量数据。
    UDP协议是一个不可靠的、无连接协议,主要适用于不需要对报文进行排序和流量控制的场合。

  4. 应用层 
    应用层面向不同的网络应用引入了不同的应用层协议。其中,有基于TCP协议的,如文件传输协议(File Transfer Protocol,FTP)、虚拟终端协议(TELNET)、超文本链接协议(Hyper Text Transfer Protocol,HTTP),也有基于UDP协议的。

类的加载机制 ?

类加载机制

  1. 什么是类加载
    类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

  2. 类的生命周期
    在这里插入图片描述

  • 加载:查找并加载类的二进制数据
    加载是类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

    1. 通过一个类的全限定名来获取其定义的二进制字节流。
    2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

    相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

    加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。

设计一段代码,使之报出StackOverFlowError

海量数字中,找到最小的100个

Java 中的锁

  • 公平锁 / 非公平锁
    公平锁是指多个线程按照申请锁的顺序来获取锁。
    非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。

对于ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁 。 new ReentrntLock(isFare);
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

  • 可重入锁
    可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
synchronized void setA() {
    Thread.sleep(1000);
    setB();
}

synchronized void setB() throws Exception{
    Thread.sleep(1000);
}

对于ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁。

对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。

  • 独享锁 / 共享锁
    独享锁是指该锁一次只能被一个线程所持有。
    共享锁是指该锁可被多个线程所持有。
    对于ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
    对于Synchronized而言,当然是独享锁。

  • 互斥锁 / 读写锁

  • 乐观锁 / 悲观锁
    悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。

乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。

从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

  • 分段锁
    分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
    我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
    当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
    分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

  • 偏向锁 / 轻量级锁 / 重量级锁
    这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
    偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
    轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
    重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

  • 自旋锁
    在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

java多线程共享变量怎么处理?

进程间的通信机制有哪些 ?

说一下B+ 树

为什么数据库索引使用B+树

Java 中 Thread.sleep() 和 Object.wait() 有什么区别?

  1. 这两个方法来自不同的类分别是,sleep() 来自Thread类,和 wait() 来自 Object类。
  2. sleep() 方法没有释放锁,而 wait() 方法释放了锁,使得其他线程可以使用同步控制块或者方法。
    sleep() 不出让系统资源;wait() 是进入线程等待池等待,出让系统资源,其他线程可以占用 CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。
  3. sleep() 方法是线程类的静态方法,调用此方法会让当前线程暂停执行指定时间.将CPU时间片分给其他线程,但是对象的锁依然保持.休眠时间结束后会自动恢复到就绪状态。
  4. wait() 是 Object 类的方法,调用对象的 wait() 方法导致当前线程放弃对象的锁,线程暂停执行,进入对象的等待池,只有调用对象的notify()方法或者notifyAll()方法时,才能唤醒等待池中的线程进入等锁池,如果线程重新获得对象的锁就可以进入到就绪状态。

为什么重写了 equals() 方法以后还要重写 hashCode() 方法?

为什么重写了equals() 还要重写 hashCode()

  1. Object 对象中的 equals() 方法对于任何非空引用值 x 和 y,当且仅当 x 和 y 引用同一个对象时,此方法才返回 true。
  2. 当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。
  3. 当obj1.equals(obj2)为 true 时,obj1.hashCode() == obj2.hashCode() 必须为 true
    当obj1.hashCode() == obj2.hashCode() 为 false 时,obj1.equals(obj2) 必须为 false
  4. 为什么要重写 equals() 呢?
    如果不重写equals,那么比较的将是对象的引用是否指向同一块内存地址,重写之后目的是为了比较两个对象的value值是否相等。
  5. 特别指出利用equals比较八大包装对象(如int,float等)和String类(因为该类已重写了equals和hashcode方法)对象时,默认比较的是值,在比较其它自定义对象时都是比较的引用地址。
  6. hashcode是用于散列数据的快速存取,如利用HashSet/HashMap/Hashtable类来存储数据时,都是根据存储对象的hashcode值来进行判断是否相同的。
  7. 这样如果我们对一个对象重写了equals,意思是只要对象的成员变量值都相等那么equals就等于true,但不重写hashcode,那么我们再new一个新的对象,当原对象.equals(新对象)等于true时,两者的hashcode却是不一样的,由此将产生了理解的不一致,如在存储散列集合时(如Set类),将会存储了两个值一样的对象,导致混淆,因此,就也需要重写hashcode()。
  8. 默认的 equals() 方法是比较两个对象的内存地址
    就拿向 HashMap 中存放自定义的键-值来说明。因为 hashCode() 是 Object 类的方法,如果不重写的话,那么 hashCode() 返回的是对象的地址。如果我们要在 hashCode 中存放自定义的键的时候,我们必须保证键的唯一性。比如我们要存放(apple1,value1),(apple1,value2),apple1和apple1 是我们自定义的类的两个对象,如果不重写 hashCode() ,因为apple1 和 apple1 是通过两次new 出来的,所以他们的 hashCode 值肯定不同。这时,我们通过键的 hashCode & (lenght-1)的到桶的位置,就会把这两个相同的对象都插入到 HashMap 当中了。而这是我们不想得到的结果。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值