本文面向
- 刚学完Java的新手们。这篇文章不讲语法,而是一些除了语法必须了解的概念。
- 将要去面试的初级工程师们。查漏补缺,以免遭遇不测。
目前由于篇幅而被挪出本文的知识点:
JDK,JRE和 JVM 的区别
- JVM:java 虚拟机,负责将编译产生的字节码转换为特定机器代码,实现一次编译多处执行;
- JRE:java运行时环境,包含了java虚拟机jvm,java基础类库。是使用java语言编写的程序运行所需要的软件环境;
- JDK:java开发工具包,是编写java程序所需的开发工具。JDK包含了JRE,同时还包含了编译器javac,调试和分析工具,JavaDoc。
Java是如何编译和执行的?
JavaCompileAndExcuteStep.png上图表示了Java代码是怎么编译和加载的
整个流程从 Java 源码开始,经过 javac 程序处理后得到类文件,这个文件中保存的是编译源码后得到的 Java 字节码。类文件是 Java 平台能处理的最小功能单位,也是把新代码传给运行中程序的唯一方式。
新的类文件通过类加载机制载入虚拟机,从而把新类型提供给解释器执行。
Object的重要方法
所有类都直接或间接扩展 java.lang.Object 类。这个类定义了很多有用的方法,而且你可以根据需求来重写这些方法。
toString( )方法
toString( ) 方法的作用是返回对象的文本表示形式。连接字符串或使用 System.out.println( ) 等方法时,会自动在对象上调用这个方法。给对象提供文本表示形式,十分利于调试或记录日志,而且精心编写的 toString( ) 方法还能给报告生成等任务提供帮助。
Object 类中的 toString( ) 方法返回的字符串由对象所属的类名和对象的十六进制形式哈希码(由 hashCode( ) 方法计算得到,本章节稍后会介绍)组成。这个默认的实现方式提供了对象的类型和标识两个基本信息,但一般并没什么用。
equals( )方法
== 运算符测试两个引用是否指向同一个对象(比较两个内存单元的内容是否一样)。如果要测试两个不同的对象是否相等,必须使用 equals( ) 方法。任何类都能覆盖 equals( ) 方法,定义专用的相等比较方式。Object.equals( ) 方法直接使用 == 运算符,只有两个对象是同一个对象时,才判定二者相等。
很多类以及自定义类的equals方法都需要重写,是需要根据场景与需求来定制的。JDK自带的许多类往往都是:
- 对比一些简单的属性值
- 再对比复杂的属性值or对比业务上最快能区分对象的值
- 再对比其他的值or对比地址、长度
主要为了将那些不匹配的情况尽快排除
hashCode( )方法
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::compareTo( )方法
如果一个类实现了 Comparable 接口,就可以比较一个实例是小于、大于还是等于另一个实例。这也表明,实现 Comparable 接口的类可以排序。
因为 compareTo( ) 方法不在 Object 类中声明,所以由每个类自行决定实例能否排序。如果能排序就定义 compareTo( ) 方法,实现实例排序的方式。
compareTo( ) 方法返回一个 int 类型的值,这个值需要进一步说明。如果当前对象(this)小于传入的对象,compareTo( ) 方法应该返回一个负数;如果两个对象相等,应该返回 0;如果当前对象大于传入的对象,应该返回一个正数。
clone( )方法
Object 类定义了一个名为 clone( ) 的方法,这个方法的作用是返回一个对象,并把这个对象的字段设为和当前对象一样。clone( ) 方法不常用,原因有两个。其一,只有类实现了 java.lang.Cloneable 接口,这个方法才有用。Cloneable 接口没有定义任何方法(是个标记接口),因此若想实现这个接口,只需在类签名的 implements 子句中列出这个接口即可。其二,clone( ) 方法声明为 protected,因此,如果想让其他类复制你的对象,你的类必须实现 Cloneable 接口,并覆盖 clone( ) 方法,而且要把 clone( ) 方法声明为 public。
clone( ) 方法很难正确实现,而副本构造方法实现起来更容易也更安全。
finalize( )方法
一种古老的资源管理技术叫终结(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 引用这个对象这一事实。
那么在用运算符:==时,也会有差别。
相等运算符(==)比较基本值时,只测试两个值是否一样(即每一位的值都完全相同)。而 == 比较引用类型时,比较的是引用而不是真正的对象。也就是说,== 测试两个引用是否指向同一个对象,而不测试两个对象的内容是否相同。
Java 的四种引用
在 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");
一个只有在运行时才能抛出异常,一个在编译期便可以发现错误。
封装类
Java中为什么要为基本类型提供封装类呢?
是为了在各种类型间转化,通过各种方法的调用。否则你无法直接通过变量转化。
比如,现在int要转为String
int a=0;
String result=Integer.toString(a);
比如现在要用泛型
List<Integer> nums;
这里< >里面就需要指定一个类。如果用int,则报错。
自动装箱(autoboxing)与拆箱(unboxing)
自动装箱是 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);
}
关于异常
JavaThrowable.jpg图是我自己做的。如果觉得子类父类傻傻分不清,可以按照“红橙黄绿”这个顺序来,最高父类是红。
- 遇上Error就是跪了,你就别想拯救了。
- Exception一般由编码、环境、用户操作输入出现问题,我们要可以捕捉的也处于这一块儿。
- 运行时异常由java虚拟机由自己捕获自己抛出。
- 检查异常则由自己捕获自己抛出多重catch,顺序是从子类到父类。
异常抛出
-
throw:将产生的异常抛出。交给上层去处理。异常链----A方法抛出异常,B方法尝试捕获。main中调用B,B捕获的异常中会有A异常的信息。
-
throws:声明将要抛出何种类型的异常。
下面是异常类族谱
ThrowableSystem.png捕捉的异常时,不要仅仅调用printStackTreace( )去打印输出,应该添加事务回滚等操作。catch(Exception)可以捕捉遗漏的异常。最后在finally语句里记得释放资源。
Java异常处理的10个最佳实践
这里是我收集的 Java 编程中异常处理的 10 个最佳实践。大家对 Java 中的受检异常(checked Exception)褒贬不一,这种语言特性要求该异常必须被处理。在本文中,我们尽可能少使用受检异常,同时也要学会在 Java 编程中,区别使用受检和非受检异常。
1 为可恢复的错误使用受检异常,为编程错误使用非受检异常。
对 Java 开发者来说,选择受检还是非受检异常总是让人感到困惑。受检异常保证你会针对错误情况提供异常处理代码,这是一种从语言层面上强制你编写健壮代码的一种方式,但同时也引入大量杂乱的代码并导致其可读性变差。当然,如果你有可替代方式或恢复策略的话,捕获异常并做处理看起来似乎也合情合理。在 Java 编程中选择受检异常还是运行时异常的更多信息,请参考 checked vs unchecked exceptions。
2 在 finally 程序块中关闭或者释放资源
这是 Java 编程中一个广为人知的最佳实践和一个事实上的标准,尤其是在处理网络和 IO 操作的时候。在 finally 块中关闭资源能保证无论是处于正常还是异常执行的情况下,资源文件都能被合理释放,这由 finally 语句块保证。从 Java7 开始,新增加了一项更有趣的功能:自动资源管理,或者称之为ARM块。尽管如此,我们仍然要记住在 finally 块中关闭资源,这对于释放像 FileDescriptors 这类资源至关重要,因为它在 socket 和文件操作中都会被用到。
3 在堆栈信息中包含引起异常的原因
Java 库和开源代码在很多情况下会将一种异常包装成另一种异常。这样记录和打印根异常就变得非常重要。Java 异常类提供了 getCause() 方法来获取导致异常的原因,这可以提供更多有关异常发生的根本原因的信息。这条实践对调试或排除故障大有帮助。在把一个异常包装成另一种异常时,记住需要把源异常传递给新异常的构造器。
4 始终提供异常的有意义的完整信息
异常信息是最重要的,在其中,你能找到问题产生的原因,因为这是出问题后程序员最先看到的地方。记得始终提供精确的真实的信息。例如,对比下面两条 IllegalArgumentException 的异常信息:
message 1: “Incorrect argument for method” message 2: “Illegal value for ${argument}: ${value}
第一条消息仅说明了参数是非法的或不正确的,但第二条消息包括了参数名和非法值,这对找到错误原因很重要。在编写异常处理代码的时候,应当始终遵循该 Java 最佳实践。
5 避免过度使用受检异常
受检异常的强制性在某种程度上具有一定的优势,但同时它也使得代码可读性变差,混淆了正常的业务逻辑代码。你可以通过适度使用受检异常来最大限度地减少这类情况的发生,这样可以得到更简洁的代码。你同样可以使用 Java7 的新功能,比如在一个catch语句中捕获多个异常,以及自动管理资源,以此来移除一些冗余的代码。
6 将受检异常转为运行时异常
这是在诸如 Spring 之类的框架中用来减少使用受检异常的方式之一,大部分 JDBC 的受检异常都被包装进 DataAccessException 中,DataAccessException异常是一种非受检异常。这个最佳实践带来的好处是可以将特定的异常限制到特定的模块中,比如把 SQLException 抛到 DAO 层,把有意义的运行时异常抛到客户端层。
7 记住异常的性能代价高昂
需要记住的一件事是异常代价高昂,同时让代码运行缓慢。假如你有一个方法从 ResultSet 中进行读取,它经常会抛出 SQLException 而不是将 cursor 移到下一元素,这将会比不抛出异常的正常代码执行的慢的多。因此最大限度的减少不必要的异常捕捉,去修复真正的根本问题。不要仅仅是抛出和捕捉异常,如果你能使用 boolean 变量去表示执行结果,可能会得到更整洁、更高性能的解决方案。修正错误的根源,避免不必要的异常捕捉。
8 避免空的 catch 块
没有什么比空的 catch 块更糟糕的了,因为它不仅隐藏了错误和异常,同时可能导致你的对象处于不可用状态或者脏状态。空的 catch 块没有意义,除非你非常肯定异常不会以任何方式影响对象的状态,但在程序执行期间,用日志记录错误依然是最好的方法。这在 Java 异常处理中不仅仅是一个最佳实践,而且是一个最通用的实践。
9 使用标准异常
第九条最佳实践是建议使用标准和内置的 Java 异常。使用标准异常而不是每次创建我们自己的异常,这对于目前和以后代码的可维护性和一致性,都是最好的选择。重用标准异常使代码可读性更好,因为大部分 Java 开发人员对标准的异常更加熟悉,比如 JDK 中的RuntimeException,IllegalStateException,IllegalArgumentException,NullPointerException,他们能立马知道每种异常的目的,而不是在代码或文档里查找用户自定义异常的目的。
10 为方法抛出的异常编写文档
Java 提供了 throw 和 throws 关键字来抛出异常,在 javadoc 中可以用@throw 为任何可能被抛出的异常编写文档。如果你编写 API 或者公共接口,这就变得非常重要。当任何方法抛出的异常都有相应的文档记录时,就能潜在的提醒任何调用该方法的开发者。
Java 创建对象的几种方式
-
用new 语句创建对象,这是最常见的创建对象的方法
-
运用反射手段,调用 java.lang.Class 或者 java.lang.reflect.Constructor 类的 newInstance( ) 实例方法
-
调用对象的 clone( ) 方法
-
运用反序列化手段,调用 java.io.ObjectInputStream 对象的 readObject( ) 方法
-
(1)和(2)都会明确的显式的调用构造函数;(3)是在内存上对已有对象的影印,所以不会调用构造函数 (4)是从文件中还原类的对象,也不会调用构造函数。
序列化(Serializable )与反序列化(Deserialize)
对象序列化(Serializable)是指将对象转换为字节序列的过程,而反序列化则是根据字节序列恢复对象的过程。
简单的来说就是从object变成了byte,用于传输。
序列化一般用于以下场景:
- 永久性保存对象,保存对象的字节序列到本地文件中;
- 通过序列化对象在网络中传递对象;
- 通过序列化在进程间传递对象。
只有实现了Serializable和Externalizable接口的类的对象才能被序列化。
小Tips:对子类对象进行反序列化操作时,如果其父类没有实现序列化接口,那么其父类的构造函数会被显式的调用。
java.io.ObjectOutputStream
代表对象输出流,它的writeObject(Objectobj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
java.io.ObjectInputStream
代表对象输入流,它的readObject( )方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
覆盖 (Override) 和重载 (Overload)
-
Override:方法覆盖是说子类重新定义了父类的方法,方法覆盖必须有相同的方法名,参数列表和返回类型。一般会有个
@Override
注解。 -
Overload:Java中的方法重载发生在同一个类里面两个或者是多个方法的方法名相同但是参数不同的情况
集合框架
对象存入集合时会变成Object类型,取出时需要类型转换。所以会有泛型(这样也不用考虑取出时的类型转换了)。另外集合里存储的是引用,所以泛型不能使用基本类型。
常见集合
集合概览
集合家族一览
-
Set 是一种 Collection,不过其中没有重复的对象;List 也是一种 Collection,其中的元素按顺序排列(不过可能有重复)。
-
SortedSet 和 SortedMap 是特殊的集和映射,其中的元素按顺序排列。
-
Collection、Set、List、Map、SortedSet 和 SortedMap 都是接口,不过 java.util 包定义了多个具体实现,例如基于数组和链表的列表,基于哈希表或二叉树的映射和集。除此之外,还有两个重要的接口:Iterator 和 Iterable,用于遍历集合中的对象。
Collection接口
Collection<e> 接口是参数化接口,表示由泛型 E 对象组成的集合。这个接口定义了很多方法,用来把对象添加到集合中,把对象从集合中移除,测试对象是否在集合中,以及遍历集合中的所有元素。还有一些方法可以把集合中的元素转换成数组,以及返回集合的大小。
Set接口
集(set)是无重复对象组成的集合:不能有两个引用指向同一个对象,或两个指向 null 的引用,如果对象 a 和 b 的引用满足条件 a.equals(b),那么这两个对象也不能同时出现在集中。多数通用的 Set 实现都不会对元素排序,但并不禁止使用有序集(SortedSet 和 LinkedHashSet 就有顺序)。而且集与列表等有序集合不同,一般认为,集的 contains 方法,不论以常数时间还是以对数时间都为1,运行效率都高。
List接口
List 是一组有序的对象集合。列表中的每个元素都有特定的位置,而且 List 接口定义了一些方法,用于查询或设定特定位置(或叫索引)的元素。从这个角度来看,List 对象和数组类似,不过列表的大小能按需变化,以适应其中元素的数量。和集不同,列表允许出现重复的元素。
除了基于索引的 get( ) 和 set( ) 方法之外,List 接口还定义了一些方法,用于把元素添加到特定的索引,把元素从特定的索引移除,或者返回指定值在列表中首次出现或最后出现的索引。从 Collection 接口继承的 add( ) 和 remove( ) 方法,前者把元素添加到列表末尾,后者把指定值从列表中首次出现的位置移除。继承的 addAll( ) 方法把指定集合中的所有元素添加到列表的末尾,或者插入指定的索引。retainAll( ) 和 removeAll( ) 方法的表现与其他 Collection 对象一样,如果需要,会保留或删除多个相同的值。
List 接口没有定义操作索引范围的方法,但是定义了一个 subList( ) 方法。这个方法返回一个 List 对象,表示原列表指定范围内的元素。子列表会回馈父列表,只要修改了子列表,父列表立即就能察觉到变化。
Map接口
映射(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接口和BlockingQueue接口
队列(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 包中定义。
网友评论