java面向对象四大特性
封装
在面向对象编程方法中,封装(英语:Encapsulation)是指,一种将抽象性函数接口的实现细节部分包装、隐藏起来的方法。同时,它也是一种防止外界调用端,去访问对象内部实现细节的手段,这个手段是由编程语言本身来提供的。封装被视为是面向对象的四项原则之一。
适当的封装,可以将对象使用接口的程序实现部分隐藏起来,不让用户看到,同时确保用户无法任意更改对象内部的重要资料,若想接触资料只能通过公开接入方法(Publicly accessible methods)的方式( 如:“getters” 和"setters")。它可以让代码更容易理解与维护,也加强了代码的安全性。
抽象
抽象是指在 OOP 中让一个类抽象的能力。一个抽象类是不能被实例化的。类的功能仍然存在,它的字段,方法和构造函数都以相同的方式进行访问。你只是不能创建一个抽象类的实例。
如果一个类是抽象的,即不能被实例化,这个类如果不是子类它将没有什么作用。这体现了在设计过程中抽象类是如何被提出的。一个父类包含子类的基本功能集合,但是父类是抽象的,不能自己去使用功能
继承
继承(英语:inheritance)是面向对象软件技术当中的一个概念。如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”。继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码。在令子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。另外,为子类追加新的属性和方法也是常见的做法。 一般静态的面向对象编程语言,继承属于静态的,意即在子类的行为在编译期就已经决定,无法在运行期扩展。
有些编程语言支持多重继承,即一个子类可以同时有多个父类,比如C++编程语言;而在有些编程语言中,一个子类只能继承自一个父类,比如Java编程语言,这时可以透过实现接口来实现与多重继承相似的效果。
现今面向对象程序设计技巧中,继承并非以继承类别的“行为”为主,而是继承类别的“类型”,使得组件的类型一致。另外在设计模式中提到一个守则,“多用合成,少用继承”,此守则也是用来处理继承无法在运行期动态扩展行为的遗憾。
多态
指为不同数据类型的实体提供统一的接口,或使用一个单一的符号来表示多个不同的类型
多态的最常见主要类别有:
特设多态:为个体的特定类型的任意集合定义一个共同接口。
参数多态:指定一个或多个类型不靠名字而是靠可以标识任何类型的抽象符号。
子类型(也叫做子类型多态或包含多态):个名字指称很多不同的类的实例,这些类有某个共同的超类。
static关键字
static 在 java 中是一个非常重要的关键字,主要用于修饰类、成员变量、成员方法,除此之外,我们还可以将代码块修饰为 static,被 static 修饰的方法或者变量不需要依赖对象的实例化,只要类被加载,被static修饰的方法或者属性就能被使用。
static 修饰成员变量
被static 修饰的成员变量被称为:静态变量。静态变量被所有对象共享,在内存中只有一个副本,在程序加载静态变量所属类的时候,静态变量就会被初始化。非静态变量是对象级别的,也就是说非静态变量的创建永远跟随着对象的实例,存在多个副本。
static 修饰方法
被 static 修饰的方法称为:静态方法。由于静态方法也不需要依赖对象,所以对于静态方法来说,是没有 this 的,并且静态变量不能访问非静态方法,原因很简单,静态方法的加载时间是跟随类的加载,而非静态方法需要对象被实例化之后才能被使用,会导致在静态方法里面根本就找不到你要调用的那么非静态方法。但是非静态方法是可以调用静态方法的,原因也很简单,非静态方法存在的时候,静态方法一定存在,但是静态方法存在的时候非静态方法不一定存在。
static 修饰代码块
被 static 修饰的代码块成为:静态代码块。静态代码块可以存在一个类的任何位置,跟随着类的加载被执行,一个类中可以存在多个静态代码块,他们严格的按照 stattic 的顺序来执行每个 静态代码块,并且每个 静态代码块只会被程序执行一次,由于这个特性,静态代码块用的好的话可以提升程序的性能。
static 误区
与 C/C++不同的是,java 中的static关键字不会影响到变量或者方法的作用域,只有 private、public、protected 可以影响 java 中变量或者方法的作用域。
final关键字
final:最终的,通常表示这是无法修改的。一个属性在什么样的情况下才会被设计成无法修改呢?可能是因为设计和效率。
final修饰的数据:表示数据是永恒不变的,比如:
一个永远不改变的编译时常量。
一个在运行时被初始化的值,而程序员不希望它被修改。
编译时常量:编译器可以将该常量带入任何可能使用到它的计算中,可以在编译时执行计算,减轻了一些运行时的负担。需要注意的是,java在定义这些常量时必须是基本数据类型,并且以 final 关键字修饰,并且需要给定初始值。
一个既是 static 优势 final 修饰的域只占用一段不饿能该百年的存储空间。
当 final 修饰的不是基本数据类型,而是引用类型时(比如:对象),这个时候容易让人犯迷糊,final 可以让基本数据类型的值永恒不变,final 可以让引用类型的引用永恒不变,但是引用对象的内容可以改变(不能将一个数据的引用地址0x1 变成 0x2,但是可以改变0x1 对象中的属性,我可以将0x1对象的 name 属性改成任意值)。
final 修饰类或方法:
修饰类:表示这个类是永恒不变的,不可以被继承、不可以被覆盖,该类下的所有方法不能被重写,该类下的所有成员变量不能被覆盖。
修饰方法:继承类无法重写被 final 修饰的方法。
private 与final
一个类中,所有的 private 方法或者属性都是被隐式的指定为 final,由于private 的访问权限,所以子类无法重写 private 的方法,我们可以对 private 方法添加 final 关键字,但是这并不能给该方法增加任何额外的意义,语法不会报错,但是这样写显得有点重复。
请注意,final 类禁止继承,所以 final 类下的所有方法都会被隐式的指定为 final ,无法重写他们。 在 final 类中可以给方法添加 final 关键字,但是没有什么意义。
建议
在设计类的时候,尽可能的将类设计成 final ,出于设计考虑,你希望不希望有人来修改这个类的任何东西,但是你无法预测是否有人会修改你的类,所以,如果你希望该类或者方法不被修改,那么最好将他设置为final 。
String常用的方法有哪些
length():返回字符串的长度。
isEmpty():判断字符串的长度是否等于0( value.length == 0)
charAt():返回指定索引下的字符。
getBytes():字符串转字节。
equals():判断两个字符串的值是否相同
equalsIgnoreCase():比较两个字符串是否相等,不区分大小写。
compareTo():比较对应字符的大小(ASCII码顺序),如果第一个字符和参数的第一个字符不等,结束比较,返回他们之间的长度差值,如果第一个字符和参数的第一个字符相等,则以第二个字符和参数的第二个字符做比较,以此类推,直至比较的字符或被比较的字符有一方结束。
startsWith():检测字符串是否以指定的前缀开始。
endsWith():检测字符串是否以指定的后缀结束。
hashCode():返回字符串的hashCode。
indexOf():返回指定字符在字符串中第一次出现处的索引,如果此字符串中没有这样的字符,则返回 -1。
lastIndexOf():返回指定字符在此字符串中最后一次出现处的索引,如果此字符串中没有这样的字符,则返回 -1。
substring():截取字符串。
concat():字符串拼接。
replace():字符串替换。
contains():判断当前字符串是否包含某个字符。
split():字符串分割,返回一个数组。
join():将集合或数组通过指定字符分割组合成字符串。
toLowerCase():将所有字符转换为小写。
toUpperCase():将所有字符转换为大写。
trim():去除字符串前后空格。
toCharArray():将此字符串转换为新的字符数组。
format():使用指定的格式字符串和参数返回格式化字符串。
valueOf():返回参数的字符串表示形式。
intern():该方法的作用是把字符串加载到常量池中(jdk1.6常量池位于方法区,jdk1.7以后常量池位于堆)
== 和 equals 的区别是什么
java中数据结构分为基本数据类型和引用类型,他们在使用 == 、equals 时的含义也不相同,我们分别来看看他们在基本数据类型和引用类型中的具体表现吧。
==
基本数据类型:比较两个值是否相同。
引用类型:比较两个对象是否相同(比较两个对象的引用地址是否相同)。
equal
Object 类中的 equals 方法和 == 的效果是一样的,有些类(String、Integer等)重写了 Object 类中的 equals 方法,将他们用于值的比较。
我们先来看看 Object 类中的 equals 方法源码
public boolean equals(Object obj) { return (this == obj);}
我们再来看看 String类中的 equals 方法
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; 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;}
所以当我们判断两个对象是否相同使用 equals 其实和使用 == 是一样的,因为对象没有重写 equals 方法(当然,我们也可以重写 equals 方法 来实现对比值是对象的属性是否相同来判断对象是否相同),而当我们使用new Stirng() 创建的字符串 通过 equals 方法进行比较时,他们对比的就是字符串的值,如果值相同就返回 true,不会对比内存地址,因为 String 重写 Object类中的 equals 方法。
重载和重写的区别?
重载:发生在同一个类中,方法名必须相同,实质表现就是多个具有不同的参数个数或者类型的同名函数(返回值类型可随意,不能以返回类型作为重载函数的区分标准),返回值类型、访问修饰符可以不同,发生在编译时。
重写: 发生在父子类中,方法名、参数列表必须相同,是父类与子类之间的多态性,实质是对父类的函数进行重新定义。返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法。
修饰符public、private、protected,以及不写(默认)时的区别?
private:私有,只有当前类有操作权。
default:当成员变量没有访问修饰符时默认为default,对于同一个包下,它相当于 public(公开),不同包下相当于 private (私有)。
protected:受保护,对同一个包中的类或者子类相当于 public(公开),对于不同包下且没有父子关系的相当于 private(私有)。
public:公开,对所有类都是公开的。
修饰符 当前类 同包 子类 其他包
private ✔ ✖ ✖ ✖
default ✔ ✔ ✖ ✖
protected ✔ ✔ ✔ ✖
public ✔ ✔ ✔ ✔
注意:
可以修饰外部类的修饰符只有 public 和 default。
public 修饰外部类时,在同一包内,可以访问,无需导包;同一包外可以访问,需要导包。
default 修饰外部类时,在同一包内,可以访问,无需导包;同一包外,无法访问。
构造器Constructor是否可被override?
构造器Constructor不能被继承,因此不能重写Override,但可以被重载Overload。
Constructor不能被继承,所以Constructor也就不能被override。每一个类必须有自己的构造函数,负责构造自己这部分的构造。子类不会覆盖父类的构造函数,相反必须负责在一开始调用父类的构造函数。
String、StringBuffer、StringBuilder的区别
String:字符串常量,字符串长度不可变。Java 中 String 是 immutable(不可变)的。
StringBuffer:字符串变量(Synchronized,即线程安全),线程安全的可变字符序列,可以改变字符序列的长度和内容,如果需要频繁的对字符串进行拼接,StringBuffer 相对于 “+” 的重载效率会高得多,如果需要将 StringBuffer 转 String,只需要调用 StringBuffer 的toString() 方法。
StringBuilder:字符串变量(非线程安全,JDK1.5 引入)。在内部,StringBuilder 对象被当作是一个包含字符序列的变长数组。此类提供一个与 StringBuffer 兼容的 API,但不保证同步。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。
三者区别
String 和 StringBuffer区别主要在性能方面,由于 String 是不可变对象,所以通过 “+” 来拼接的字符串都会生成一个新的 String 类,然后将指针指向新的 String 对象。这并不能说明 “+” 的性能就弱于StringBuffer,我们知道,在 JDK 1.5 之后引入了 StringBuilder, “+” 是通过 StringBuilder 实现,对于一般的字符串拼接,“+” 和 StringBuilder 的性能其实没有什么差别,因为 JVM 会对 String 的 “+” 做优化,既然这样的话,是否就没有必要使用 StringBuffer 和 StringBuilder 了呢?不是的,如果你在某个循环中对字符串进行拼接, “+” 会在循环内构造 StringBuilder ,也就是说,循环了多少次,StringBuilder 就被创建了多少次。而 StringBuffer 和 StringBuilder 在循环拼接的时候只需要创建一个对象即可,性能非常的明显。
对于 StringBuffer 和 StringBuilder ,他们都是 AbstractStringBuilder 抽象类的子类,都有着相同的实现,主要区别在于 StringBuffer 线程安全,而 StringBuilder 非线程安全,这也必然会导致 StringBuilder 的性能高于 StringBuffer 。
最后 String、StringBuffer、StringBuilder 的性能由高到低:StringBuilder > StringBuffer > String。
final, finally, finalize的区别
final:用于声明属性,方法和类,分别表示属性不可交变,方法不可覆盖,类不可继承。请参考前文中的final关键字的介绍。
finally:Java 中的 Finally 关键一般与try一起使用,在程序进入try块之后,无论程序是因为异常而终止或其它方式返回终止的,finally块的内容一定会被执行 。
finalize:是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,供垃圾收集时的其他资源回收,例如关闭文件等。
通常来说,这三个关键字之前没有什么必然的联系,只是名字有点像,给人感觉他们很相似。
字节流与字符流的区别
字节流就是普通的二进制流,读出来的是bit。
字符流就是在字节流的基础按照字符编码处理,处理的是char。
什么是java序列化,如何实现java序列化?或者请解释Serializable接口的作用
Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。
将序列化对象写入文件之后,可以从文件中读取出来,并且对它进行反序列化,也就是说,对象的类型信息、对象的数据,还有对象中的数据类型可以用来在内存中新建对象。
简单来说:序列化就是将java对象转成字节流的过程,反序列化则是将字节流转成java对象的过程。
java实现序列化
原生序列化:java对象实现 Serializable 接口,通过原生流( InputStream/outputStream) 实现。
JSON序列化:spring提供了一种默认的json序列化工具包:jackson ,通过ObjectMapper类来实现对象转byte[] 数组或者 json 串转对象的过程。
FastJson:阿里巴巴旗下一个员工因为没有好用的序列化工具开发的,虽然目前该工具 bug 满天飞,但是使用 FastJson 的公司还是不少。
ProtoBuff序列化: protobuf(Google Protocol Buffers)是Google提供一个具有高效的协议数据交换格式工具库(类似Json),但相比于Json,Protobuf有更高的转化效率,时间效率和空间效率都是JSON的3-5倍。后面将会有简单的demo对于这两种格式的数据转化效率的对比。
abstract class和interface有什么区别?
abstract class:含有 abstract 修饰符的class即为抽象类,abstract 类不能创建实例对象,含有 abstract 修饰的方法的类必须是 abstract 类,但是 abstract 类并不要求其所有方法都是抽象方法。抽象类中定义的抽象方法必须在继承的子类中实现,所以,不能有抽象构造方法或者抽象静态方法。如果子类没有实现父类(抽象类)的所有抽象方法,那么该子类也必须定义为抽象类。
interface:可以说成是抽象类的一种特例,接口中的所有方法都必须是抽象的。接口中的方法定义默认为public abstract类型,接口中的成员变量类型默认为public static final。
a=a+b与a+=b有什么区别吗?
+= 操作符会进行隐式自动类型转换,此处 a+=b 隐式的将加操作的结果类型强制转换为持有结果的类型,而 a=a+b 则不会自动进行类型转换。
从图片中我们可以看出: a+b 的结果应该是 int类型,第一组操作 a += b;之所以没有报错是因为编译器给我自动做了类型转换(将int 类型强转为 short 类型),而 a=a+b 没有做类型转换,所以编译无法通过。
一个类能拥有多个main方法吗?
可以,但只能拥有一个这样的main方法
public static void main(String[] args) {}
否者程编译将无法通过,警告你的main方法已存在。
java支持什么类型的参数传递?
直接说结论吧,java只有值传递,没有引用传递,解释的话有点长,感兴趣的可以参考一下我之前写的文章:天真,居然还有人认为java的参数传递方式是引用传递。
这里说一下什么是引用传递、什么是值传递
什么是引用传递?
在C++中,函数参数的传递方式有引用传递。所谓引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
什么是值传递?
值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
集合
Arraylist 和 LinkedList的区别?
arrayList
ArrayList 是集合框架的一部分,存在于 java.util 包中。它为我们提供了 Java 中的动态数组。
虽然,它可能比标准数组慢,但在需要对数组进行大量操作的程序中很有帮助。此类位于java.util包中。
特性
ArrayList 容器大小可以默认(10)也可以指定,如果数据增多或者减少,ArrayList 容器也会触发扩容或者缩减操作(ArrayList 的扩容通过数据拷贝的方式实现,浅拷贝,所以我们在使用 ArrayList 的时候最好指明容器大小,如果触发扩容次数较多,会严重影响性能)。
ArrayList 支持随机访问(下标访问),且效率非常高,时间复杂度:O(1),但是按需查找(查找值),效率和链表一样,时间复杂度为:O(n)。
ArrayList 只能被用于包装类型,基本类型不可用。
ArrayList 线程不安全的,也就是不同步,可以使用 Vector代替。
ArrayList 工作流程:ArrayList通过构造函数创建了一个容器(默认容器大小 10),调用 add() 方法可以往 ArrayList 中的 elementData 数组中添加数据,
当添加的数据到达10个的时候,就会触发扩容操作,扩容一般是将容器扩大到原来的1.5倍(自己指定扩容的除外,手动扩容会将容器大小变成你指定的大小),
将老的数组拷贝到新数组中,然后废弃老数组。删除数据时,会将数组中指定的元素置空,然后做判断,如果删除的元素不是在最后一个,那么执行一遍数据拷贝,新数组的大小 -1。
LinkedList
链表是一种线性数据结构,其中元素不存储在连续的内存位置。链表中的元素使用指针链接,链表由节点组成,每个节点中至少包含本身数据和指向下一个节点的指针。
单向链表
双向链表
双链表是链表的一种,由节点组成,每个数据结点中都有两个指针,分别指向直接后继和直接前驱。
LinkedList 特点
LinkedList是双向链表实现的List(双向链表指的是每个节点都会记录前一个节点和下一个节点的指针)。
LinkedList 非线程安全。
LinkedList元素允许为null,允许重复元素
插入删除效率高,时间复杂度:O(1),查找效率一般,时间复杂度:O(n)。
不存在扩容的说法,添加数据直接在尾节点后添加,删除直接将上一个节点的下一个节点指针指向删除节点下一个节点的地址(有点绕。可以仔细品一下)。
ArrayList 和 LinkedList的区别
数据结构不同,ArrayList 基于数组;LinkedList 基于链表。
效率不同:随机访问 ArrayList 快,增加删除 LinkedList 快。
限制不同:ArrayList 由于基于数组,所以需要动态扩容,LinkedList 基于链表,不存在扩容的说法。
占用空间不同:ArrayList 需要申请一块连续的内存地址(数组的原因),LinkedList 对内存连续性没有要求(链表的原因)。
HashMap、HashSet、HashTable的区别?
HashMap
HashMap 是一个散列表,它存储的内容是键值对(key-value)映射,数据结构主要是:数组+链表的方式存储,jdk1.8 之后引入了红黑树用来优化链表过长问题。
HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步。
HashMap 是无序的,即不会记录插入的顺序。
HashMap 继承于AbstractMap,实现了 Map、Cloneable、java.io.Serializable 接口。
HashSet
HashSet 实现了 Set 接口,从他的构造函数中可以得知他也是一个 HashMap,所以他也是无序的,但是它允许空元素,虽然多个null不会报错,但是由于set的重复判断的条件,HashSet 集合中只会出现一条 null 数据,其他null会被视为重复数据,同时,由于 HashSet 依赖HashMap实现,所以它也是线程不安全的。我们再来看看 HashSet 的几个重要特性。
实现了集合接口
由于 HashSet 实现了 Set 接口,所以 HashSet 不允许出现重复数据。
HashSet 插入的对象无法保证以相同的顺序插入,插入位置由 HashCode 决定。
HashSet 允许出现 null 值。
HashSet 还实现了Serializable和Cloneable接口。
HashTable
HashTable 与HashMap 类似,它们存储的内容都是键值对(key-value)映射,不过 HashTable 不允许将空值作为key或者value,从哈希表中查询或者存储对象时,用作键的对象必须实现 hashCode() 方法和 equails()方法。由于HasTable 的添加操作在方法上加上了 synchronized 同步锁,所以他是线程安全的。HashTable 特点如下
与HashMap相似,但它线程安全。
Hashtable 将键/值对存储在哈希表中。
HashTable 类的初始默认容器是11(HashMap 初始容器为 16 ),loadFactor(装载因子) 0.75。
Hashtable 提供非快速失败的枚举,而HashMap没有。
三者区别
线程安全性:HashTable 由于加了 synchronized 同步锁,所以他是线程安全的,HashMap与 HashSet 则是非线程安全的。
数据重复:HashSet 由于实现了 Set接口,所以它不允许出现重复的值,HashMap中不允许出现重复的键,但是可以出现重复的值,HashTable也是如此,并且HashTable还不允许将空值作为key或者value。
HashMap是线程安全的吗?有什么替代集合吗?
HashMap不是线程安全的
替代品:
HashTable:通过在方法上添加 synchronized 同步锁来保证线程安全,效率偏低。
Collections.synchronizedMap(Map):通过对象排斥锁 mutex 保证线程安全,效率偏低。
ConcurrentHashMap:CAS +分段锁的机制保证线程安全,相比前面两种,它的效率会好很多。
HashMap如何解决哈希冲突问题?
hash算法
哈希算法是将任意长度的二进制值映射为较短的固定长度的二进制值,这个小的二进制值称为哈希值(java 通过 hash 将会得到一个整数 )。
哈希值是一段数据唯一且极其紧凑的数值表示形式。如果散列一段明文而且哪怕只更改该段落的一个字母,随后的哈希都将产生不同的值。要找到散列为同一个值的两个不同的输入,在计算上是不可能的,所以数据的哈希值可以检验数据的完整性。一般用于快速查找和加密算法。
hash碰撞
Hash算法并不完美,有可能两个不同的原始值在经过哈希运算后得到同样的结果, 这样就是哈希碰撞。
我们希望的 hash:
散列函数计算得到的散列值是一个非负整数。
如果 key1 = key2,那 hash(key1) == hash(key2)。
如果 key1 ≠ key2,那 hash(key1) ≠ hash(key2)。
前面两点没啥说的,key 相同,hash计算出来的值必须一致,但是至于最后一点,想要找到一个不同 key 对应散列值(hash值)也都不一样的散列(hash)函数,那是几乎不可能的。即便像业界著名的MD5、SHA、CRC等哈希算法,也无法完全避免这种散列冲突。而且,因为数组的存储空间有限,也会加大散列冲突的概率。
解决方案
开放寻址法
如果出现了 hash 冲突,那我们就重新探测一个空闲位置,然后再插入,二如何重新探测新的插入位置呢?线性探测就能解决这个问题。线性探测指的是:如果某个 key 经过 散列函数得到的下标已经被占用了,这个时候就从当前位置出发,后移一位,判断位置是否空闲,空闲则插入,否则继续重复探测。
如图所示:黄色的色块表示空闲位置,橙色的色块表示已经存储了数据。
但是这里存在一个问题,需要注意,有可能会被面试官挖坑哦。存储已经说完了,现在说说查找,查找和插入有点类似,我们通过散列函数求出需要查找元素的键值对应的散列值。然后比较数组中下标与计算出来下标的元素是否相同,如果相同,说明找到了,如果不相同,就会往后顺序一次查找,直到查找到数组中的空闲位置,说明查找的元素并不存在当前数组中。这点很重要,一定要牢记。这本身并没有什么问题,但是结合元素的删除,问题就出现了,我们不能直接将需要删除的元素置空,为什么呢?我们在查找时,如果通过线性探索的方式找到一个空闲的位置,那么就代表查找的元素不在数组中,假设数组中,下标 0、1、2分别村的数据是:zhangsan、lisi、wangwu、,这个时候我们将下标 1 质控,下标 2 计算出来的散列值原本是 0 ,通过线性探索 存放在 2 的下标,这个时候我们查找wangwu 的时候找到下标 为0 的元素,发现不相等,这个时候就会往后查找,但不巧的是,下标 1 被你置空了,所以查询结束,wangwu 不在当前数组中。这个坑一定要记住哦,解决方案很简单,将删除的元素做一个标识即可。但是线性探测存在比较大问题,当散列表中的数据越来越多时,发生散列冲突的概率就会越高,空闲的位置越来越少,极端情况下,需要扫描整个三散列表,所以先行探索的最坏时间复杂度为:O(n)。可以考虑另外两种探索方案:二次探索、双重探索(感兴趣的自己百度)。
链表法
目前 HashMap 解决散列冲突使用的就是链表法。相比于开放寻址法,链表法简单得多,只需要引用数组即可。在散列表中,每个桶或者槽都会对应一条链表,将发生散列冲突的元素存储到链表中即可。
插入:通过散列函数计算出散列值,如果发现当前下标已经存在元素(hash碰撞),这个时候将插入的元素插入到当前下标对应的链表中即可,效率非常的高:O(1)。插入的效率虽然很高,那删除和查询的效率如何?
查找、删除也需要计算出当前元素的散列值,如果和当前下标存储的元素不同,则需要遍历与之对应的整条链表。遍历链表的时间复杂度与链表的长度 k成正比,也就是 O(k) ,对于散列比较均匀的散列函数来说,理论上讲,k=n/m,其中 n 表示散列中数据的个数,m 表示散列表中“槽”的个数。
这里又存在一个问题,当hash碰撞过于频繁,导致某条链表过长,这个时候查找、删除的时间复杂度就是直线上升,官方人员也发现了这个问题,所以在 JDK 1.8 的时候引入了红黑树来防止链表过长导致效率降低问题。
为什么重写equals时必须重写hashCode方法?
我们经常听到的一句话:重写对象 equals() 方法时,必须也要重写hashCode() 方法,主要原因是什么请看下面介绍。
先说几个概念:
两个对象相等,hashCode一定相同。
两个对象不等,hashCode 不一定不相同。
hashCode 相同,两个对象不一定相等。
hashCode 不同,两个对象一定不等。
为什么会出现上面这四句话呢?首先我们要知道什么是 hashCode,hashCode 主要用于计算字符串的哈希码,什么?你问我什么是哈希码?请自行百度,我们以HashMap为例,get() /containsKey() 都会存在一个 hash(key) 这样的方法,返回一个整形数字,这个方法首先会计算出HashMap 中 key 的哈希值,然后在计算出当前 key 存储的数组下标地址,这样HashMap 就能快速的查询出当前 key 的数据或者校验当前key是否存在,之所以这样能查出来是因为存的时候也是按照这种方式计算的,比我们遍历HashMap取值快得多,计算一个 key 算出的哈希值是唯一的,那为什么还会出现上面的几种概念呢?那是因为不同的 key 算出来的哈希值有可能是一样的,我们还是以HashMap为例, 第一个 key 通过计算 得到的 下标为 6 ,第二个 key 通过计算得到的下标也是 6,这个时候就出现了哈希碰撞,HashMap通过引入链表的方式进行处理,所以这里可以看出 hashCode 虽然相同,但是 key 却不一定相同,理解了这个概念,其他三个概念就不难理解了,这下知道为什么重写 equals 时必须重写 hashCode 方法了吧。我在附上String类中 hashCode() 和 HashMap 的 hash() 方法源码。
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value;
for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }