本文面向html
刚学完Java的新手们。这篇文章不讲语法,而是一些除了语法必须了解的概念。java
将要去面试的初级工程师们。查漏补缺,以避免遭遇不测。程序员
目前因为篇幅而被挪出本文的知识点:面试
Java反射学习小记segmentfault
JVM:java 虚拟机,负责将编译产生的字节码转换为特定机器代码,实现一次编译多处执行;缓存
JRE:java运行时环境,包含了java虚拟机jvm,java基础类库。是使用java语言编写的程序运行所须要的软件环境;安全
JDK:java开发工具包,是编写java程序所需的开发工具。JDK包含了JRE,同时还包含了编译器javac,调试和分析工具,JavaDoc。
上图表示了Java代码是怎么编译和加载的
整个流程从 Java 源码开始,通过 javac 程序处理后获得类文件,这个文件中保存的是编译源码后获得的 Java 字节码。类文件是 Java 平台能处理的最小功能单位,也是把新代码传给运行中程序的惟一方式。
新的类文件经过类加载机制载入虚拟机,从而把新类型提供给解释器执行。
全部类都直接或间接扩展 java.lang.Object 类。这个类定义了不少有用的方法,并且你能够根据需求来重写这些方法。
toString( ) 方法的做用是返回对象的文本表示形式。链接字符串或使用 System.out.println( ) 等方法时,会自动在对象上调用这个方法。给对象提供文本表示形式,十分利于调试或记录日志,并且精心编写的 toString( ) 方法还能给报告生成等任务提供帮助。
Object 类中的 toString( ) 方法返回的字符串由对象所属的类名和对象的十六进制形式哈希码(由 hashCode( ) 方法计算获得,本章节稍后会介绍)组成。这个默认的实现方式提供了对象的类型和标识两个基本信息,但通常并没什么用。
== 运算符测试两个引用是否指向同一个对象(比较两个内存单元的内容是否同样)。若是要测试两个不一样的对象是否相等,必须使用 equals( ) 方法。任何类都能覆盖 equals( ) 方法,定义专用的相等比较方式。Object.equals( ) 方法直接使用 == 运算符,只有两个对象是同一个对象时,才断定两者相等。
不少类以及自定义类的equals方法都须要重写,是须要根据场景与需求来定制的。JDK自带的许多类每每都是:
对比一些简单的属性值
再对比复杂的属性值or对比业务上最快能区分对象的值
再对比其余的值or对比地址、长度
主要为了将那些不匹配的状况尽快排除
Java中的hashCode方法就是根据必定的规则将与对象相关的信息(好比对象的存储地址,对象的字段等)映射成一个数值,这个数值称做为散列值。 若是集合中已经存在一万条数据或者更多的数据,若是采用equals方法去逐一比较,效率必然是一个问题。此时hashCode方法的做用就体现出来了,当集合要添加新的对象时,先调用这个对象的hashCode方法,获得对应的hashcode值,实际上在HashMap的具体实现中会用一个table保存已经存进去的对象的hashcode值,若是table中没有该hashcode值,它就能够直接存进去,不用再进行任何比较了;若是存在该hashcode值,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址,因此这里存在一个冲突解决的问题,这样一来实际调用equals方法的次数就大大下降了。
另外注意,默认的hashCode会发起native调用,若是用hashCode对两个对象对比,会致使开销增大。
hashcode方法的做用
只要覆盖了 equals( ) 方法,就必须覆盖 hashCode( ) 方法。hashCode( ) 方法返回一个整数,用于哈希表数据结构。若是两个对象经 equals( ) 方法测试是相等的,它们就要具备相同的哈希码。不相等的对象要具备不相等的哈希码(为了哈希表的操做效率),这一点很重要,但不是强制要求,最低要求是不相等的对象不能共用一个哈希码。为了知足最低要求,hashCode( ) 方法要使用稍微复杂的算法或位操做。
Object.hashCode( ) 方法和 Object.equals( ) 方法协同工做,返回对象的哈希码。这个哈希码基于对象的身份生成,而不是对象的相等性。(若是须要使用基于身份的哈希码,能够经过静态方法 System.identityHashCode( ) 获取 Object.hashCode( ) 方法的返回值。)
hashCode和equal方法
hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中肯定对象的存储地址的;
若是两个对象相同,就是适用于equals(java.lang.Object) 方法,那么这两个对象的hashCode必定要相同;
若是对象的equals方法被重写,那么对象的hashCode也尽可能重写,而且产生hashCode使用的对象,必定要和equals方法中使用的一致,不然就会违反上面提到的第2点;
两个对象的hashCode相同,并不必定表示两个对象就相同,也就是不必定适用于equals(java.lang.Object)方法,只可以说明这两个对象在散列存储结构中,如Hashtable,他们"存放在同一个篮子里"。
若是一个类实现了 Comparable 接口,就能够比较一个实例是小于、大于仍是等于另外一个实例。这也代表,实现 Comparable 接口的类能够排序。
由于 compareTo( ) 方法不在 Object 类中声明,因此由每一个类自行决定实例可否排序。若是能排序就定义 compareTo( ) 方法,实现实例排序的方式。
compareTo( ) 方法返回一个 int 类型的值,这个值须要进一步说明。若是当前对象(this)小于传入的对象,compareTo( ) 方法应该返回一个负数;若是两个对象相等,应该返回 0;若是当前对象大于传入的对象,应该返回一个正数。
Object 类定义了一个名为 clone( ) 的方法,这个方法的做用是返回一个对象,并把这个对象的字段设为和当前对象同样。clone( ) 方法不经常使用,缘由有两个。其一,只有类实现了 java.lang.Cloneable 接口,这个方法才有用。Cloneable 接口没有定义任何方法(是个标记接口),所以若想实现这个接口,只需在类签名的 implements 子句中列出这个接口便可。其二,clone( ) 方法声明为 protected,所以,若是想让其余类复制你的对象,你的类必须实现 Cloneable 接口,并覆盖 clone( ) 方法,并且要把 clone( ) 方法声明为 public。
clone( ) 方法很难正确实现,而副本构造方法实现起来更容易也更安全。
一种古老的资源管理技术叫终结(finalization),开发者应该知道有这么一种技术。然而,这种技术几乎彻底废弃了,任何状况下,大多数 Java 开发者都不该该直接使用。
只有少数应用场景适合使用终结,并且只有少数 Java 开发者会遇到这种场景。若是有任何疑问,就不要使用终结,处理资源的 try 语句每每是正确的替代品。
终结机制的做用是自动释放再也不使用的资源。垃圾回收自动释放的是对象使用的内存资源,不过对象可能会保存其余类型的资源,例如打开的文件和网络链接。垃圾回收程序不会为你释放这些额外的资源,所以,终结机制的做用是让开发者执行清理任务,例如关闭文件、中断网络链接、删除临时文件,等等。
终结机制的工做方式是这样的:若是对象有 finalize( ) 方法(通常叫做终结方法),那么再也不使用这个对象(或对象不可达)后的某个时间会调用这个方法,但要在垃圾回收程序回收分配给这个对象的空间以前调用。终结方法用于清理对象使用的资源。
另外注意,这是一个实例方法。而在类上,没有等效的机制。
type | which |
---|---|
基础 | byte short int long float double char boolean |
引用 | 数组 对象 |
8种基本类型对应的包装类也是被final修饰。另外,String类和StringBuffer类也是被final修饰的。
引用类型和对象与基本类型和基本值有本质的区别。
八种基本类型由 Java 语言定义,程序员不能定义新基本类型。引用类型由用户定义,所以有无限多个。例如,程序能够定义一个名为 Point 的类,而后使用这个新定义类型的对象存储和处理笛卡儿坐标系中的 (x, y) 点。
基本类型表示单个值。引用类型是聚合类型(aggregate type),能够保存零个或多个基本值或对象。例如,咱们假设的 Point 类可能存储了两个 double 类型的值,表示点的 x 和 y 坐标。char[ ] 和 Point[ ] 数组类型是聚合类型,由于它们保存一些 char 类型的基本值或 Point 对象。
基本类型须要一到八个字节的内存空间。把基本值存储到变量中,或者传入方法时,计算机会复制表示这个值的字节。而对象基本上须要更多的内存。建立对象时会在堆(heap)中动态分配内存,存储这个对象;若是再也不须要使用这个对象了,存储它的内存会被自动垃圾回收。
把对象赋值给变量或传入方法时,不会复制表示这个对象的内存,而是把这个内存的引用存储在变量中或传入方法。
在 Java 中,引用彻底不透明,引用的表示方式由 Java 运行时的实现细节决定。若是你是 C 程序员的话,彻底能够把引用看做指针或内存地址。不过要记住,Java 程序没法使用任何方式处理引用。
彷佛看的有点晕?来点儿代码吧!
下述代码处理 int 类型基本值:
int x = 42; int y = x;
执行这两行代码后,变量 y 中保存了变量 x 中所存值的一个副本。在 Java 虚拟机内部,这个 32 位整数 42 有两个独立的副本。
如今,想象一下把这段代码中的基本类型换成引用类型后再运行会发生什么:
Point p = new Point(1.0, 2.0); Point q = p;
运行这段代码后,变量 q 中保存了一份变量 p 中所存引用的一个副本。在虚拟机中,仍然只有一个 Point 对象的副本,可是这个对象的引用有两个副本----这一点有重要的含义。假设上面两行代码的后面是下述代码:
System.out.println(p.x); // 打印p的x坐标:1.0 q.x = 13.0; // 如今,修改q的x坐标 System.out.println(p.x); // 再次打印p.x,此次获得的值是13.0
由于变量 p 和 q 保存的引用指向同一个对象,因此两个变量均可以用来修改这个对象,并且一个变量中的改动在另外一个变量中可见。数组也是一种对象,因此对数组来讲也会发生一样的事,以下面的代码所示:
// greet保存一个数组的引用 char[ ] greet = { 'h','e','l','l','o' }; char[ ] cuss = greet; // cuss保存的是同一个数组的引用 cuss[4] = '!'; // 使用引用修改一个元素 System.out.println(greet); // 打印“hell!”
把基本类型和引用类型的参数传入方法时也有相似的区别。假若有下面的方法:
void changePrimitive(int x) { while(x > 0) { System.out.println(x--); } }
调用这个方法时,会把实参的副本传给形参 x。在这个方法的代码中,x 是循环计数器,向零递减。由于 x 是基本类型,因此这个方法有这个值的私有副本——这是彻底合理的作法。
但是,若是把这个方法的参数改成引用类型,会发生什么呢?
void changeReference(Point p) { while(p.x > 0) { System.out.println(p.x--); } }
调用这个方法时,传入的是一个 Point 对象引用的私有副本,而后使用这个引用修改对应的 Point 对象。例如,有下述代码:
Point q = new Point(3.0, 4.5); // 一个x坐标为3的点 changeReference(q); // 打印3,2,1,并且修改了这个Point对象 System.out.println(q.x); // 如今,q的x坐标是0!
调用 changeReference( ) 方法时,传入的是变量 q 中所存引用的副本。如今,变量 q 和方法的形参 p 保存的引用指向同一个对象。这个方法可使用它的引用修改对象的内容。可是要注意,这个方法不能修改变量 q 的内容。也就是说,这个方法能够随意修改引用的 Point 对象,但不能改变变量 q 引用这个对象这一事实。
那么在用运算符:==时,也会有差异。
相等运算符(==)比较基本值时,只测试两个值是否同样(即每一位的值都彻底相同)。而 == 比较引用类型时,比较的是引用而不是真正的对象。也就是说,== 测试两个引用是否指向同一个对象,而不测试两个对象的内容是否相同。
在 JDK1.2 后,Java 对引用概念扩充,分为强引用、软引用、弱引用、虚引用。强度渐弱。
在开始了解前,最好先稍微了解一下Java Memory Model。个人这篇文章中简单的讲了一下JMM
就是值在程序代码之中广泛存在的,相似 Object obj = new Object() 这类的引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。
它关联着的对象,在系统将要发生内存溢出异常以前,将会把这些对象列进回收范围内进行第二次回收。提供 SoftReference 类来实现软引用。
强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生以前。提供 WeakReference 类来实现软引用。
一个对象是否有虚引用的存在,彻底不会对其生存时间构成影响,也没法经过虚引用来去的一个对象实例。为一个对象设置虚引用关联的惟一目的就是能在这个对象被收集器回收时收到一个系统通知。提供 PhantomReference 类来实现软引用。
Java 7之基础 - 强引用、弱引用、软引用、虚引用
Java垃圾回收机制与引用类型
数组类型不是类,但数组实例是对象。这意味着,数组从 java.lang.Object 类继承了方法。数组实现了 Cloneable 接口,并且覆盖了 clone( ) 方法,确保数组始终能被复制,并且 clone( ) 方法从不抛出 CloneNotSupportedException 异常。数组还实现了 Serializable 接口,因此只要数组中元素的类型能被序列化,数组就能被序列化。并且,全部数组都有一个名为 length 的字段,这个字段的修饰符是 public final int,表示数组中元素的数量。
由于数组扩展自 Object 类,并且实现了 Cloneable 和 Serializable 接口,因此任何数组类型都能放大转换成这三种类型中的任何一种。并且,特定的数组类型还能放大转换成其余数组类型。若是数组中的元素类型是引用类型 T,并且 T 能指定给类型 S,那么数组类型 T[ ] 就能指定给数组类型 S[ ]。注意,基本类型的数组不能放大转换。例如,下述代码展现了合法的数组放大转换:
String[ ] arrayOfStrings; // 建立字符串数组 int[ ][ ] arrayOfArraysOfInt; // 建立int二维数组 Object[ ] oa = arrayOfStrings;// String能够指定给Object,所以String[ ]能够指定给Object[ ] Comparable[ ] ca = arrayOfStrings;// String实现了Comparable接口,所以String[ ]能够视做Comparable[ ] Object[ ] oa2 = arrayOfArraysOfInt;// int[ ]是Object类的对象,所以int[ ][ ]能够指定给Object[ ] // 全部数组都是能够复制和序列化的对象 Object o = arrayOfStrings; Cloneable c = arrayOfArraysOfInt; Serializable s = arrayOfArraysOfInt[0];
由于数组类型能够放大转换成另外一种数组类型,因此编译时和运行时数组的类型并不老是同样。这种放大转换叫做"数组协变"(array covariance)。
因此在某种意义上,集合框架比数组好用:
Object [] objectArray = new Long[1]; objectArray[0] = "I dont fit in"; //Throws ArrayStoreException
List<Object> ol = new ArrayList<Long>(); //Incompatible types ol.add("I dont fit in");
一个只有在运行时才能抛出异常,一个在编译期即可以发现错误。
是为了在各类类型间转化,经过各类方法的调用。不然你没法直接经过变量转化。
好比,如今int要转为String
int a=0; String result=Integer.toString(a);
好比如今要用泛型
List<Integer> nums;
这里< >里面就须要指定一个类。若是用int,则报错。
自动装箱是 Java 编译器在基本数据类型和对应的对象包装类型之间作的一个转化。
基本类型和引用类型的表现彻底不一样。有时须要把基本值当成对象,为此,Java 平台为每一种基本类型都提供了包装类。Boolean、Byte、Short、Character、Integer、Long、Float 和 Double 是不可变的最终类,每一个实例只保存一个基本值。包装类通常在把基本值存储在集合中时使用。 例如
java.util.List: List numbers =newArrayList( );// 建立一个List集合 numbers.add(newInteger(-1));// 存储一个包装类表示的基本值 int i =((Integer)numbers.get(0)).intValue( );// 取出这个基本值
把 int 转化成 Integer,double 转化成 Double等,反之就是自动拆箱。
Integer a=1;//这就是一个自动装箱,若是没有自动装箱的话,须要这样Integer a=new Integer(1) int b=a;//这就是一个自动拆箱,若是没有自动拆箱的话,须要这样:int b=a.intValue( )
这样就能看出自动装箱和自动拆箱是简化了基本数据类型和相对应对象的转化步骤。
自动拆装箱将会致使性能问题,由于有些数字不属于缓存范围——意味着会产生新的对象,尤为是在集合框架中会严重致使性能降低。
请运行一下下面的代码,并探究一下:
public static void main(String []args){ Integer a = 1; Integer b = 1; Integer c = 200; Integer d = 200; System.out.println(a==b); System.out.println(c==d); }
图是我本身作的。若是以为子类父类傻傻分不清,能够按照“红橙黄绿”这个顺序来,最高父类是红。
赶上Error就是跪了,你就别想拯救了。
Exception通常由编码、环境、用户操做输入出现问题,咱们要能够捕捉的也处于这一起。
运行时异常由java虚拟机由本身捕获本身抛出。
检查异常则由本身捕获本身抛出多重catch,顺序是从子类到父类。
throw:将产生的异常抛出。交给上层去处理。异常链----A方法抛出异常,B方法尝试捕获。main中调用B,B捕获的异常中会有A异常的信息。
throws:声明将要抛出何种类型的异常。
下面是异常类族谱
捕捉的异常时,不要仅仅调用printStackTreace( )去打印输出,应该添加事务回滚等操做。catch(Exception)能够捕捉遗漏的异常。最后在finally语句里记得释放资源。
这里是我收集的 Java 编程中异常处理的 10 个最佳实践。你们对 Java 中的受检异常(checked Exception)褒贬不一,这种语言特性要求该异常必须被处理。在本文中,咱们尽量少使用受检异常,同时也要学会在 Java 编程中,区别使用受检和非受检异常。
对 Java 开发者来讲,选择受检仍是非受检异常老是让人感到困惑。受检异常保证你会针对错误状况提供异常处理代码,这是一种从语言层面上强制你编写健壮代码的一种方式,但同时也引入大量杂乱的代码并致使其可读性变差。固然,若是你有可替代方式或恢复策略的话,捕获异常并作处理看起来彷佛也合情合理。在 Java 编程中选择受检异常仍是运行时异常的更多信息,请参考 checked vs unchecked exceptions。
这是 Java 编程中一个广为人知的最佳实践和一个事实上的标准,尤为是在处理网络和 IO 操做的时候。在 finally 块中关闭资源能保证不管是处于正常仍是异常执行的状况下,资源文件都能被合理释放,这由 finally 语句块保证。从 Java7 开始,新增长了一项更有趣的功能:自动资源管理,或者称之为ARM块。尽管如此,咱们仍然要记住在 finally 块中关闭资源,这对于释放像 FileDescriptors 这类资源相当重要,由于它在 socket 和文件操做中都会被用到。
Java 库和开源代码在不少状况下会将一种异常包装成另外一种异常。这样记录和打印根异常就变得很是重要。Java 异常类提供了 getCause() 方法来获取致使异常的缘由,这能够提供更多有关异常发生的根本缘由的信息。这条实践对调试或排除故障大有帮助。在把一个异常包装成另外一种异常时,记住须要把源异常传递给新异常的构造器。
异常信息是最重要的,在其中,你能找到问题产生的缘由,由于这是出问题后程序员最早看到的地方。记得始终提供精确的真实的信息。例如,对比下面两条 IllegalArgumentException 的异常信息:
message 1: “Incorrect argument for method” message 2: “Illegal value for ${argument}: ${value}
第一条消息仅说明了参数是非法的或不正确的,但第二条消息包括了参数名和非法值,这对找到错误缘由很重要。在编写异常处理代码的时候,应当始终遵循该 Java 最佳实践。
受检异常的强制性在某种程度上具备必定的优点,但同时它也使得代码可读性变差,混淆了正常的业务逻辑代码。你能够经过适度使用受检异常来最大限度地减小这类状况的发生,这样能够获得更简洁的代码。你一样可使用 Java7 的新功能,好比在一个catch语句中捕获多个异常,以及自动管理资源,以此来移除一些冗余的代码。
这是在诸如 Spring 之类的框架中用来减小使用受检异常的方式之一,大部分 JDBC 的受检异常都被包装进 DataAccessException 中,DataAccessException异常是一种非受检异常。这个最佳实践带来的好处是能够将特定的异常限制到特定的模块中,好比把 SQLException 抛到 DAO 层,把有意义的运行时异常抛到客户端层。
须要记住的一件事是异常代价高昂,同时让代码运行缓慢。假如你有一个方法从 ResultSet 中进行读取,它常常会抛出 SQLException 而不是将 cursor 移到下一元素,这将会比不抛出异常的正常代码执行的慢的多。所以最大限度的减小没必要要的异常捕捉,去修复真正的根本问题。不要仅仅是抛出和捕捉异常,若是你能使用 boolean 变量去表示执行结果,可能会获得更整洁、更高性能的解决方案。修正错误的根源,避免没必要要的异常捕捉。
没有什么比空的 catch 块更糟糕的了,由于它不只隐藏了错误和异常,同时可能致使你的对象处于不可用状态或者脏状态。空的 catch 块没有意义,除非你很是确定异常不会以任何方式影响对象的状态,但在程序执行期间,用日志记录错误依然是最好的方法。这在 Java 异常处理中不只仅是一个最佳实践,并且是一个最通用的实践。
第九条最佳实践是建议使用标准和内置的 Java 异常。使用标准异常而不是每次建立咱们本身的异常,这对于目前和之后代码的可维护性和一致性,都是最好的选择。重用标准异常使代码可读性更好,由于大部分 Java 开发人员对标准的异常更加熟悉,好比 JDK 中的RuntimeException,IllegalStateException,IllegalArgumentException,NullPointerException,他们能立马知道每种异常的目的,而不是在代码或文档里查找用户自定义异常的目的。
Java 提供了 throw 和 throws 关键字来抛出异常,在 javadoc 中能够用@throw 为任何可能被抛出的异常编写文档。若是你编写 API 或者公共接口,这就变得很是重要。当任何方法抛出的异常都有相应的文档记录时,就能潜在的提醒任何调用该方法的开发者。
用new 语句建立对象,这是最多见的建立对象的方法
运用反射手段,调用 java.lang.Class 或者 java.lang.reflect.Constructor 类的 newInstance( ) 实例方法
调用对象的 clone( ) 方法
运用反序列化手段,调用 java.io.ObjectInputStream 对象的 readObject( ) 方法
(1)和(2)都会明确的显式的调用构造函数;(3)是在内存上对已有对象的影印,因此不会调用构造函数 (4)是从文件中还原类的对象,也不会调用构造函数。
对象序列化(Serializable)是指将对象转换为字节序列的过程,而反序列化则是根据字节序列恢复对象的过程。
简单的来讲就是从object变成了byte,用于传输。
序列化通常用于如下场景:
永久性保存对象,保存对象的字节序列到本地文件中;
经过序列化对象在网络中传递对象;
经过序列化在进程间传递对象。
只有实现了Serializable和Externalizable接口的类的对象才能被序列化。
小Tips:对子类对象进行反序列化操做时,若是其父类没有实现序列化接口,那么其父类的构造函数会被显式的调用。
java.io.ObjectOutputStream
表明对象输出流,它的writeObject(Objectobj)方法可对参数指定的obj对象进行序列化,把获得的字节序列写到一个目标输出流中。
java.io.ObjectInputStream
表明对象输入流,它的readObject( )方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
Override:方法覆盖是说子类从新定义了父类的方法,方法覆盖必须有相同的方法名,参数列表和返回类型。通常会有个@Override
注解。
Overload:Java中的方法重载发生在同一个类里面两个或者是多个方法的方法名相同可是参数不一样的状况
对象存入集合时会变成Object类型,取出时须要类型转换。因此会有泛型(这样也不用考虑取出时的类型转换了)。另外集合里存储的是引用,因此泛型不能使用基本类型。
常见集合
集合概览
集合家族一览
Set 是一种 Collection,不过其中没有重复的对象;List 也是一种 Collection,其中的元素按顺序排列(不过可能有重复)。
SortedSet 和 SortedMap 是特殊的集和映射,其中的元素按顺序排列。
Collection、Set、List、Map、SortedSet 和 SortedMap 都是接口,不过 java.util 包定义了多个具体实现,例如基于数组和链表的列表,基于哈希表或二叉树的映射和集。除此以外,还有两个重要的接口:Iterator 和 Iterable,用于遍历集合中的对象。
Collection<e> 接口是参数化接口,表示由泛型 E 对象组成的集合。这个接口定义了不少方法,用来把对象添加到集合中,把对象从集合中移除,测试对象是否在集合中,以及遍历集合中的全部元素。还有一些方法能够把集合中的元素转换成数组,以及返回集合的大小。
集(set)是无重复对象组成的集合:不能有两个引用指向同一个对象,或两个指向 null 的引用,若是对象 a 和 b 的引用知足条件 a.equals(b),那么这两个对象也不能同时出如今集中。多数通用的 Set 实现都不会对元素排序,但并不由止使用有序集(SortedSet 和 LinkedHashSet 就有顺序)。并且集与列表等有序集合不一样,通常认为,集的 contains 方法,不论以常数时间仍是以对数时间都为1,运行效率都高。
List 是一组有序的对象集合。列表中的每一个元素都有特定的位置,并且 List 接口定义了一些方法,用于查询或设定特定位置(或叫索引)的元素。从这个角度来看,List 对象和数组相似,不过列表的大小能按需变化,以适应其中元素的数量。和集不一样,列表容许出现重复的元素。
除了基于索引的 get( ) 和 set( ) 方法以外,List 接口还定义了一些方法,用于把元素添加到特定的索引,把元素从特定的索引移除,或者返回指定值在列表中首次出现或最后出现的索引。从 Collection 接口继承的 add( ) 和 remove( ) 方法,前者把元素添加到列表末尾,后者把指定值从列表中首次出现的位置移除。继承的 addAll( ) 方法把指定集合中的全部元素添加到列表的末尾,或者插入指定的索引。retainAll( ) 和 removeAll( ) 方法的表现与其余 Collection 对象同样,若是须要,会保留或删除多个相同的值。
List 接口没有定义操做索引范围的方法,可是定义了一个 subList( ) 方法。这个方法返回一个 List 对象,表示原列表指定范围内的元素。子列表会回馈父列表,只要修改了子列表,父列表当即就能察觉到变化。
映射(map)是一系列键值对,一个键对应一个值。Map 接口定义了用于定义和查询映射的 API。Map 接口属于 Java 集合框架,但没有扩展 Collection 接口,所以 Map 只是一种集合,而不是 Collection 类型。Map 是参数化类型,有两个类型变量。类型变量 K 表示映射中键的类型,类型变量 V 表示键对应的值的类型。例如,若是有个映射,其键是 String 类型,对应的值是 Integer 类型,那么这个映射能够表示为 Map<string,integer>
Map 接口定义了几个最有用的方法:put( ) 方法定义映射中的一个键值对,get( ) 方法查询指定键对应的值,remove( ) 方法把指定的键及对应的值从映射中删除。通常来讲,实现 Map 接口的类都要能高效执行这三个基本方法:通常应该运行在常数时间中,并且毫不能比在对数时间中运行的性能差。
Map 的重要特性之一是,能够视做集合。虽然 Map 对象不是 Collection 类型,但映射的键能够当作 Set 对象,映射的值能够当作 Collection 对象,而映射的键值对能够当作由 Map.Entry 对象组成的 Set 对象。(Map.Entry 是 Map 接口中定义的嵌套接口,表示一个键值对。)
队列(queue)是一组有序的元素,提取元素时按顺序从队头读取。队列通常按照插入元素的顺序实现,所以分红两类:先进先出(first-in, first-out,FIFO)队列和后进先出(last-in, first-out,LIFO)队列。
LIFO 队列也叫栈(stack),Java 提供了 Stack 类,但强烈不建议使用,应该使用实现 Deque 接口的类。
队列也可使用其余顺序:优先队列(priority queue)根据外部 Comparator 对象或 Comparable 类型元素的天然顺序排序元素。与 Set 不一样的是,Queue 的实现每每容许出现重复的元素。而与 List 不一样的是,Queue 接口没有定义处理任意索引位元素的方法,只有队列的头一个元素能访问。Queue 的全部实现都要具备一个固定的容量:队列已满时,不能再添加元素。相似地,队列为空时,不能再删除元素。不少基于队列的算法都会用到满和空这两个状态,因此 Queue 接口定义的方法经过返回值代表这两个状态,而不会抛出异常。具体而言,peek( ) 和 poll( ) 方法返回 null 表示队列为空。所以,多数 Queue 接口的实现不容许用 null 做元素。
阻塞式队列(blocking queue)是一种定义了阻塞式 put( ) 和 take( ) 方法的队列。put( ) 方法的做用是把元素添加到队列中,若是须要,这个方法会一直等待,直到队列中有存储元素的空间为止。而 take( ) 方法的做用是从队头移除元素,若是须要,这个方法会一直等待,直到队列中有元素可供移除为止。阻塞式队列是不少多线程算法的重要组成部分,所以 BlockingQueue 接口(扩展 Queue 接口)在 java.util.concurrent 包中定义。