异常机制
导引问题
工作中,程序遇到的情况不可能完美。比如:程序要打开某个文件,这个文件可能不存在或者文件格式不对;程序在运行着,但是内存或硬盘可能满了等等。
软件程序在运行过程中,非常可能遇到刚刚提到的这些问题,我们称之为异常,英文是:Exception,意思是例外。遇到这些例外情况,或者叫异常,我们怎么让写的程序做出合理的处理,安全的退出,而不至于程序崩溃呢?我们这里就要讲解这些问题。
如果我们要拷贝一个文件,在没有异常机制的情况下,我们需要考虑各种异常情况,伪代码如下:
【示例】伪代码:使用 if 处理程序中可能出现的各种情况
这种方式,有两个坏处:
- 逻辑代码和错误处理代码放一起!
- 程序员本身需要考虑的例外情况较复杂,对程序员本身要求较高!
如上情况,如果是用 Java 的异常机制来处理,对比如下:
异常机制本质
当程序出现异常,程序安全的退出、处理完后继续执行的机制
异常(Exception)的概念
异常指程序运行过程中出现的非正常现象,例如除数为零、需要处理的文件不存在、数组下标越界等。
在 Java 的异常处理机制中,引进了很多用来描述和处理异常的类,称为异常类。异常类定义中包含了该类异常的信息和对异常进行处理的方法。
我们开始看我们的第一个异常对象,并分析一下异常机制是如何工作的。
【示例】异常的分析
public class Test {
public static void main(String[] args) {
System.out.println("111");
int a = 1/0;
System.out.println("222");
}
}
执行结果如图所示:
根据结果,我们可以看到执行“1/0”时发生了异常,程序终止了,没有执行后面的打印“222”的动作。
如果我们使用 try-catch 来处理,程序遇到异常可以正常的处理,处理完成后,程序继续往下执行:
public class Test {
public static void main(String[] args) {
System.out.println("111");
try {
int a = 1/0;
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("222");
}
}
执行结果如下:
程序在执行“1/0”仍然遇到异常,然后进行 try-catch 处理。处理完毕后,程序继续往下执行,打印了“222”内容。
Java 是采用面向对象的方式来处理异常的。处理过程:
- 抛出异常:在执行一个方法时,如果发生异常,则这个方法生成代表 该异常的一个对象,停止当前执行路径,并把异常对象提交给 JRE。
- 捕获异常:JRE 得到该异常后,寻找相应的代码来处理该异常。JRE 在方法的调用栈中查找,从生成异常的方法开始回溯,直到找到相应的异常处理代码为止。
异常分类
Java 中定义了很多异常类,这些类对应了各种各样可能出现的异常事件,所有异常对象都是派生于 Throwable 类的一个实例。如果内置的异常类不能够满足需要,还可以创建自己的异常类。
Java 对异常进行了分类,不同类型的异常分别用不同的 Java 类表示,所有异常的根类为 java.lang.Throwable
,Throwable 下面又派生了两个子类:Error 和 Exception。Java异常类的层次结构如图所示:
Error
Error 是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java 虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
Error 表明系统 JVM 已经处于不可恢复的崩溃状态中。
Error 与 Exception 的区别
我开着车走在路上,一头猪冲在路中间,我刹车。这叫一个异常。
我开着车在路上,发动机坏了,我停车,这叫错误。系统处于不可恢复的崩溃状态。发动机什么时候坏?我们普通司机能管吗?不能。发动机什么时候坏是汽车厂发动机制造商的事。
Exception
Exception 是程序本身能够处理的异常。
Exception 类是所有异常类的父类,其子类对应了各种各样可能出现的异常事件。 通常 Java 的异常可分为:
- RuntimeException 运行时异常
- CheckedException 已检查异常
RuntimeException 运行时异常
派生于 RuntimeException 的异常,如被 0 除、数组下标越界、空指针等,其产生比较频繁,处理麻烦,如果显式的声明或捕获将会对程序可读性和运行效率影响很大。因此由系统自动检测并将它们交给缺省的异常处理程序。
编译器不处理 RuntimeException, 程序员需要增加“逻辑处理来避免这些异常”。
【示例】ArithmeticException 异常:试图除以 0
public class Test3 {
public static void main(String[ ] args) {
int b=0;
System.out.println(1/b);
}
}
执行结果如图所示:
解决如上异常需要修改代码:
public class Test3 {
public static void main(String[ ] args) {
int b=0;
if(b!=0){
System.out.println(1/b);
}
}
}
【示例】NullPointerException 异常
public class Test4 {
public static void main(String[ ] args) {
String str=null;
System.out.println(str.charAt(0));
}
}
执行结果如图所示:
解决空指针异常,通常是增加非空判断:
public class Test4 {
public static void main(String[ ] args) {
String str=null;
if(str!=null){
System.out.println(str.charAt(0));
}
}
}
【示例】ClassCastException 异常
class Animal{
}
class Dog extends Animal{
}
class Cat extends Animal{
}
public class Test5 {
public static void main(String[ ] args) {
Animal a=new Dog();
Cat c=(Cat)a;
}
}
执行结果如图所示:
解决 ClassCastException 的典型方式:
public class Test5 {
public static void main(String[ ] args) {
Animal a = new Dog();
if (a instanceof Cat) {
Cat c = (Cat) a;
}
}
}
【示例】ArrayIndexOutOfBoundsException 异常
public class Test6 {
public static void main(String[ ] args) {
int[ ] arr = new int[5];
System.out.println(arr[5]);
}
}
执行结果如图所示:
解决数组索引越界异常的方式,增加关于边界的判断:
public class Test6 {
public static void main(String[ ] args) {
int[ ] arr = new int[5];
int a = 5;
if (a < arr.length) {
System.out.println(arr[a]);
}
}
}
【示例】NumberFormatException 异常
public class Test7 {
public static void main(String[ ] args) {
String str = "1234abcf";
System.out.println(Integer.parseInt(str));
}
}
执行结果如图所示:
数字格式化异常的解决,可以引入正则表达式判断是否为数字:
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Test7 {
public static void main(String[ ] args) {
String str = "1234abcf";
Pattern p = Pattern.compile("^\\d+$");
Matcher m = p.matcher(str);
if (m.matches()) { // 如果 str 匹配代表数字的正则表达式,才会转换
System.out.println(Integer.parseInt(str));
}
}
}
CheckedException 已检查异常
CheckedException 异常在编译时就必须处理,否则无法通过编译。如图所示。
CheckedException 异常的处理方式有两种:
- 使用“try/catch”捕获异常
- 使用“throws”声明异常。
异常的处理方式之一:捕获异常
- try:
try 语句指定了一段代码,该段代码就是异常捕获并处理的范围。在执行过程中,当任意一条语句产生异常时,就会跳过该条语句中后面的代码。代码中可能会产生并抛出一种或几种类型的异常对象,它后面的 catch 语句要分别对这些异常做相应的处理。
一个 try 语句必须带有至少一个 catch 语句块或一个 finally 语句块。
注意事项
当异常处理的代码执行结束以后,不会回到 try 语句去执行尚未执行的代码。
catch:
每个 try 语句块可以伴随一个或多个 catch 语句,用于处理可能产生的不同类型的异常对象。
catch 捕获异常时的捕获顺序:
如果异常类之间有继承关系,先捕获子类异常再捕获父类异常。
finally:
不管是否发生了异常,都必须要执行。
通常在 finally 中关闭已打开的资源,比如:关闭文件流、释放数据库连接等。
try-catch-finally 语句块的执行过程详细分析:
程序首先执行可能发生异常的 try
语句块。如果 try
语句没有出现异常则执行完后跳至finally
语句块执行;如果 try
语句出现异常,则中断执行并根据发生的异常类型跳至相应的catch
语句块执行处理。catch
语句块可以有多个,分别捕获不同类型的异常。catch
语句块执行完后程序会继续执行 finally
语句块。finally
语句是可选的,如果有的话,则不管是否发生异常,finally
语句都会被执行。
【示例】异常处理的典型代码(捕获异常)
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class Test8 {
public static void main(String[ ] args) {
FileReader reader = null;
try {
reader = new FileReader("d:/a.txt");
char c = (char) reader.read();
char c2 = (char) reader.read();
System.out.println("" + c + c2);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (reader != null) {
reader.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
常用开发环境中,自动增加 try-catch
代码块的快捷键:
- 将需要处理异常的代码选中。
- IDEA 中,使用:
ctrl+alt+t
- eclipse 中,使用:
ctrl+shift+z
异常的处理方式之二:声明异常(throws 子句)
- CheckedException 产生时,不一定立刻处理它,可以把异常 throws,由调用者处理。
- 一个方法抛出多个已检查异常,就必须在方法的首部列出所有的异常。
【示例】异常处理的典型代码(声明异常抛出 throws)
package com.bjsxt;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class Test9 {
public static void main(String[ ] args) {
try {
readFile("joke.txt");
} catch (FileNotFoundException e) {
System.out.println("所需文件不存在!");
} catch (IOException e) {
System.out.println("文件读写错误!");
}
}
public static void readFile(String fileName) throws
FileNotFoundException,
IOException {
FileReader in = new FileReader(fileName);
int tem = 0;
try {
tem = in.read();
while (tem != -1) {
System.out.print((char) tem);
tem = in.read();
}
} finally {
if(in!=null) {
in.close();
}
}
}
}
注意事项
方法重写中声明异常原则:子类重写父类方法时,如果父类方法有声明异常,那么子类声明的异常范围不能超过父类声明的范围。
try-with-resource 自动关闭 AutoClosable 接口的资源
JAVA 中,JVM 的垃圾回收机制可以对内部资源实现自动回收,给开发者带来了极大的便利。但是 JVM 对外部资源(调用了底层操作系统的资源)的引用却无法自动回收,例如数据库连接,网络连接以及输入输出 IO 流等。这些连接就需要我们手动去关闭,不然会导致外部资源泄露,连接池溢出以及文件被异常占用等。
JDK7 之后,新增了“ try-with-resource”。它可以自动关闭实现了
AutoClosable 接口的类,实现类需要实现 close()方法。”try-with-resources 声明”,将 try-catch-finally 简化为 try-catch,这其实是一种语法糖,在编译时仍然会进行转化为 try-catch-finally 语句。
package com.bjsxt;
import java.io.FileReader;
public class Test8 {
public static void main(String[ ] args) {
try(FileReader reader = new FileReader("d:/a.txt");) {
char c = (char) reader.read();
char c2 = (char) reader.read();
System.out.println("" + c + c2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
自定义异常
- 在程序中,可能会遇到 JDK 提供的任何标准异常类都无法充分描述清楚我们想要表达的问题,这种情况下可以创建自己的异常类,即自定义异常类。
- 自定义异常类只需从 Exception 类或者它的子类派生一个子类即可。
- 自定义异常类如果继承 Exception 类,则为 CheckedException 异常,必须对其进行处理;如果不想处理,可以让自定义异常类继承运行时异常RuntimeException 类。
- 习惯上,自定义异常类应该包含 2 个构造器:一个是默认的构造器,另一个是带有详细信息的构造器。
【示例】自定义异常类
/**IllegalAgeException:非法年龄异常,继承 Exception 类*/
public class IllegalAgeException extends Exception {
//默认构造器
public IllegalAgeException() {
}
//带有详细信息的构造器,信息存储在 message 中
public IllegalAgeException(String message) {
super(message);
}
}
【示例】自定义异常类的使用
class Person {
private String name;
private int age;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) throws IllegalAgeException {
if (age < 0) {
throw new IllegalAgeException("人的年龄不应该为负数");
}
this.age = age;
}
public String toString() {
return "name is " + name + " and age is " + age;
}
}
public class TestMyException {
public static void main(String[ ] args) {
Person p = new Person();
try {
p.setName("Lincoln");
p.setAge(-1);
} catch (IllegalAgeException e) {
e.printStackTrace();
}
System.out.println(p);
}
}
执行结果如图所示:
使用异常机制的建议
要避免使用异常处理代替错误处理,这样会降低程序的清晰性,并且效率低下。
处理异常不可以代替简单测试—只在异常情况下使用异常机制。
不要进行小粒度的异常处理—应该将整个任务包装在一个 try 语句块中。
异常往往在高层处理(先了解!后面做项目会说!) 。
如何利用百度解决异常问题
正常学习和开发中,我们经常会遇到各种异常。遇到异常时,需要遵循下面四步来解决:
- 细心查看异常信息,确定异常种类和相关 Java 代码行号
- 确定上下文相关的一些关键词信息(疑难问题,需要)。拷贝异常信息到百度,查看相关帖子,寻找解决思路;
- 前两步无法搞定,再问同学/老师或同事;
- 前三步无法搞定,请示领导。
很多同学碰到异常一下就慌了,立刻开始请教别人搬救兵,殊不知这样做有两大坏处。
第一、太不尊重别人,把别人当苦力。第二、失去提高自我的机会,自己解决一个异常,就意味着有能力解决一类异常。解决一类异常能大大提高自身能力。
不要怕花时间在解决问题上,不要觉得解决问题是耽误时间。解决问题的过程中,本身你也在思考。
· 百度超级搜索:
百度/Google 搜索用好的关键是:关键词的确认,正确的提问。
- 寻找问题本身的关键词(名词)
- 寻找问题上下文的关键词(名词)
- 尽量细致的描述问题,开始搜索
- 如果没找到,慢慢减少关键词,扩大搜索范围。
IDEA 调试 debug
调试的核心是断点。程序执行到断点时,暂时挂起,停止执行。就像看视频按下停止一样,我们可以详细的观看停止处的每一个细节。
断点 breakpoint
程序运行到此处,暂时挂起,停止执行。我们可以详细在此时观察程序的运行情况,方便做出进一步的判断。
- 设置断点:
(1) 在行号后面单击即可增加断点
(2) 在断点上再单击即可取消断点
进入调试视图
我们通过如下三种方式都可以进入调试视图:
(1) 单击工具栏上的按钮:
(2) 右键单击编辑区,点击:debug
进入调试视图后,布局如下:
左侧为“浏览帧”:
调试器列出断点处,当前线程正在运行的方法,每个方法对应一个“栈帧”。最上面的是当前断点所处的方法。
变量值观察区:
调试器列出了断点处所在方法相关的变量值。我们可以通过它,查看变量的值的变化。
调试操作区
我们通过上图中的按钮进行调试操作,它们的含义如下:
常用类
基本数据类型的包装类
我们前面学习的八种基本数据类型并不是对象,为了将基本类型数据和对象之间实现互相转化,Java 为每一个基本数据类型提供了相应的包装类。
包装类基本知识
Java 是面向对象的语言,但并不是“纯面向对象”的,因为我们经常用到的基本数据类型就不是对象。但是我们在实际应用中经常需要将基本数据转化成对象,以便于操作。比如:将基本数据类型存储到 Object[ ]
数组或集合中的操作等等。
为了解决这个不足,Java 在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八个和基本数据类型对应的类统称为包装类(Wrapper Class)。
包装类位于 java.lang 包,八种包装类和基本数据类型的对应关系:
在这八个类名中,除了 Integer
和 Character
类以外,其它六个类的类名和基本数据类型一致,只是类名的第一个字母大写而已。
Number 类是抽象类,因此它的抽象方法,所有子类都需要提供实现。Number 类提供了抽象方法:intValue()、longValue()、floatValue()、doubleValue(),意味着所有的“数字型”包装类都可以互相转型。
下面我们通过一个简单的示例认识一下包装类。
【示例】初识包装类
public class WrapperClassTest {
public static void main(String[ ] args) {
Integer i = new Integer(10); //从 java9 开始被废弃
Integer j = Integer.valueOf(50); //官方推荐
}
}
示例内存分析如图所示:
包装类的用途
对于包装类来说,这些类的用途主要包含两种:
- 作为和基本数据类型对应的类型存在,方便涉及到对象的操作,如 Object[ ]、集合等的操作。
- 包含每种基本数据类型的相关属性如最大值、最小值等,以及相关的操作方法(这些操作方法的作用是在基本数据类型、包装类对象、字符串之间提供相互之间的转化!)。
【示例】包装类的使用
public class Test {
/** 测试 Integer 的用法,其他包装类与 Integer 类似 */
void testInteger() {
// 基本类型转化成 Integer 对象
Integer int1 = new Integer(10); //已经被废弃,不推荐使用
Integer int2 = Integer.valueOf(20); // 官方推荐这种写法
// Integer 对象转化成 int
int a = int1.intValue();
// 字符串转化成 Integer 对象
Integer int3 = Integer.parseInt("334");
Integer int4 = new Integer("999");
// Integer 对象转化成字符串
String str1 = int3.toString();
// 一些常见 int 类型相关的常量
System.out.println("int 能表示的最大整数:" + Integer.MAX_VALUE);
}
public static void main(String[ ] args) {
Test test = new Test();
test.testInteger();
}
}
执行结果如图所示:
自动装箱和拆箱
自动装箱(autoboxing)和拆箱(unboxing):将基本数据类型和包装类自动转换。
自动装箱:
基本类型的数据处于需要对象的环境中时,会自动转为“对象”。
我们以 Integer 为例:
Integer i = 5
编译器会自动转成:Integer i = Integer.valueOf(5)
,这就是 Java 的自动装箱。
自动拆箱:
每当需要一个值时,对象会自动转成基本数据类型,没必要再去显式调用 intValue()、doubleValue()等转型方法。
Integer i = Integer.valueOf(5);
int j = i;
编译器会自动转成:int j = i.intValue();
这样的过程就是自动拆箱。
自动装箱/拆箱的本质是:
自动装箱与拆箱的功能是编译器来帮忙,编译器在编译时依据您所编写的语法,决定是否进行装箱或拆箱动作。
【示例】自动装箱
Integer i = 100;//自动装箱
//相当于编译器自动为您作以下的语法编译:
Integer i = Integer.valueOf(100);//调用的是 valueOf(100),而不是 new Integer(100)
【示例】自动拆箱
Integer i = 100;
int j = i;//自动拆箱
//相当于编译器自动为您作以下的语法编译:
int j = i.intValue();
自动装箱与拆箱的功能是所谓的“编译器蜜糖(Compiler Sugar)”,虽然使用这个功能很方便,但在程序运行阶段您得了解 Java 的语义。如下所示的程序是可以通过编译的:
【示例】包装类空指针异常问题
public class Test1 {
public static void main(String[ ] args) {
Integer i = null;
int j = i;
}
}
执行结果如图所示:
运行结果之所以会出现空指针异常,是因为如上代码相当于:
public class Test1 {
public static void main(String[ ] args) {
/*示例 8-5 的代码在编译时期是合法的,但是在运行时期会有错误因为其相当于下面两行代码*/
Integer i = null;
int j = i.intValue();
}
}
包装类的缓存问题
整型、char
类型所对应的包装类,在自动装箱时,对于-128~127
之间的值会进行缓存处理,其目的是提高效率。
缓存原理为:如果数据在-128~127
这个区间,那么在类加载时就已经为该区间的每个数值创建了对象,并将这256个对象存放到一个名为cache
的数组中。每当自动装箱过程发生时(或者手动调用valueOf()
时),就会先判断数据是否在该区间,如果在则直接获取数组中对应的包装类对象的引用,如果不在该区间,则会通过new调用包装类的构造方法来创建对象。
下面我们以Integer类为例,看一看Java为我们提供的源码,加深对缓存技术的理解,如示例所示。
【示例】Integer 类相关源码
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
这段代码中我们需要解释下面几个问题:
- IntegerCache类为Integer类的一个静态内部类,仅供Integer类使用。
- 一般情况下 IntegerCache.low为-128,IntegerCache.high为127,
IntegerCache.cache为内部类的一个静态属性,如示例所示。
【示例】IntegerCache 类相关源码
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[ ];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
由上面的源码我们可以看到,静态代码块的目的就是初始化数组cache的,这个过程会在类加载时完成。
下面我们做一下代码测试,如示例所示。
【示例 8-9】包装类的缓存测试
public class Test3 {
public static void main(String[ ] args) {
Integer in1 = -128;
Integer in2 = -128;
System.out.println(in1 == in2);//true 因为 123 在缓存范围内
System.out.println(in1.equals(in2));//true
Integer in3 = 1234;
Integer in4 = 1234;
System.out.println(in3 == in4);//false 因为 1234 不在缓存范围内
System.out.println(in3.equals(in4));//true
}
}
总结
自动装箱调用的是 valueOf()方法,而不是 new Integer()方法。
自动拆箱调用的 xxxValue()方法。
包装类在自动装箱时为了提高效率,对于-128~127 之间的值会进行缓存处理。超过范围后,对象之间不能再使用==进行数值的比较,而是使用 equals 方法。
自定义一个简单的包装类
public class MyInteger {
private int value;
private static MyInteger[] cache = new MyInteger[256];
public static final int LOW = -128;
public static final int HIGH = 127;
static {
//[-128,127]
for(int i=MyInteger.LOW;i<=HIGH;i++){
//-128,0;-127,1;-126,2;
cache[i+128] = new MyInteger(i);
}
}
public static MyInteger valueOf(int i) {
if(i>=LOW&&i<=HIGH) {
return cache[i+128];
}
return new MyInteger(i);
}
@Override
public String toString() {
return this.value+"";
}
public int intValue(){
return value;
}
private MyInteger(int i) {
this.value = i;
}
public static void main(String[] args) {
MyInteger m = MyInteger.valueOf(30);
System.out.println(m);
}
}
字符串相关类
String
类代表不可变的字符序列
StringBuilder
类和 StringBuffer
类代表可变字符序列。
这三个类的用法,在笔试面试以及实际开发中经常用到,必须掌握好。
String 类源码分析
String 类对象代表不可变的 Unicode 字符序列,因此我们可以将 String 对象称为“不可变对象”。 那什么叫做“不可变对象”呢?指的是对象内部的成员变量的值无法再改变。
我们打开 String 类的源码,如图所示:
我们发现字符串内容全部存储到 value[ ]数组中,而变量 value 是 final 类型的,也就是常量(即只能被赋值一次)。 这就是“不可变对象”的典型定义方式。
我们发现在前面学习 String 的某些方法,比如:substring()是对字符串的截取操作,但本质是读取原字符串内容生成了新的字符串。测试代码如下:
【示例】String 类的简单使用
public class TestString1 {
public static void main(String[ ] args) {
String s1 = new String("abcdef");
String s2 = s1.substring(2, 4);
// 打印:ab199863
System.out.println(Integer.toHexString(s1.hashCode()));
// 打印:c61, 显然 s1 和 s2 不是同一个对象
System.out.println(Integer.toHexString(s2.hashCode()));
}
}
在遇到字符串常量之间的拼接时,编译器会做出优化,即在编译期间就会完成字符串的拼接。因此,在使用==
进行 String 对象之间的比较时,我们要特别注意,如示例所示。
【示例】字符串常量拼接时的优化
public class TestString2 {
public static void main(String[ ] args) {
//编译器做了优化,直接在编译的时候将字符串进行拼接
String str1 = "hello" + " java";//相当于 str1 = "hello java";
String str2 = "hellojava";
System.out.println(str1 == str2);//true
String str3 = "hello";
String str4 = " java";
//编译的时候不知道变量中存储的是什么,所以没办法在编译的时候优化
String str5 = str3 + str4;
System.out.println(str2 == str5);//false
}
}
StringBuffer 和 StringBuilder 可变字符序列
StringBuffer 和 StringBuilder 都是可变的字符序列。
- StringBuffer 线程安全,做线程同步检查, 效率较低。
- StringBuilder 线程不安全,不做线程同步检查,因此效率较高。建议采用该类。
· 常用方法列表:
- 重载的 public StringBuilder append(…)方法
- 可以为该 StringBuilder 对象添加字符序列,仍然返回自身对象。
- 方法 public StringBuilder delete(int start,int end)
- 可以删除从 start 开始到 end-1 为止的一段字符序列,仍然返回自身对象。
- 方法 public StringBuilder deleteCharAt(int index)
- 移除此序列指定位置上的 char,仍然返回自身对象。
- 重载的 public StringBuilder insert(…)方法可以为该StringBuilder 对象在指定位置插入字符序列,仍然返回自身对象。
- 方法 public StringBuilder reverse()
- 用于将字符序列逆序,仍然返回自身对象。
- 方法 public String toString() 返回此序列中数据的字符串表示形式。
- 和 String 类含义类似的方法:
public int indexOf(String str)
public int indexOf(String str,int fromIndex)
public String substring(int start)
public String substring(int start,int end)
public int length()
char charAt(int index)
【示例】StringBuffer/StringBuilder 基本用法
public class TestStringBufferAndBuilder{
public static void main(String[ ] args) {
/**StringBuilder*/
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 7; i++) {
sb.append((char) ('a' + i));//追加单个字符
}
System.out.println(sb.toString());//转换成 String 输出
sb.append(", I can sing my abc!");//追加字符串
System.out.println(sb.toString());
/**StringBuffer,下面的方法同样适用 StringBuilder*/
StringBuffer sb2 = new StringBuffer("北京尚学堂");
sb2.insert(0, "爱").insert(0, "我");//插入字符串
System.out.println(sb2);
sb2.delete(0, 2);//删除子字符串
System.out.println(sb2);
sb2.deleteCharAt(0).deleteCharAt(0);//删除某个字符
System.out.println(sb2.charAt(0));//获取某个字符
System.out.println(sb2.reverse());//字符串逆序
}
}
执行结果如图所示:
不可变和可变字符序列使用陷阱
· String 使用的陷阱
String 一经初始化后,就不会再改变其内容了。对 String 字符串的操作实际上是对其副本(原始拷贝)的操作,原来的字符串一点都没有改变。比如:
String s ="a"; 创建了一个字符串
s = s+“b”; 实际上原来的"a"字符串对象已经丢弃了,现在又产生了另一个字符串 s+“b”(也就是"ab")。 如果多次执行这些改变串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。如果这样的操作放到循环中,会极大影响程序的时间和空间性能,甚至会造成服务器的崩溃。
相反,StringBuilder 和 StringBuffer 类是对原字符串本身操作的,可以对字符串进行修改而不产生副本拷贝或者产生少量的副本。因此可以在循环中使用。
【示例】String 和 StringBuilder 在字符串频繁修改时的效率测试
public class Test{
public static void main(String[ ] args) {
/**使用 String 进行字符串的拼接*/
String str8 = "";
long num1 = Runtime.getRuntime().freeMemory();//获取系统剩余内存空间
long time1 = System.currentTimeMillis();//获取系统的当前时间
for (int i = 0; i < 5000; i++) {
str8 = str8 + i;//相当于产生了 5000 个对象
}
long num2 = Runtime.getRuntime().freeMemory();
long time2 = System.currentTimeMillis();
System.out.println("String 占用内存 : " + (num1 - num2));
System.out.println("String 占用时间 : " + (time2 - time1));
/**使用 StringBuilder 进行字符串的拼接*/
StringBuilder sb1 = new StringBuilder("");
long num3 = Runtime.getRuntime().freeMemory();
long time3 = System.currentTimeMillis();
for (int i = 0; i < 5000; i++) {
sb1.append(i);
}
long num4 = Runtime.getRuntime().freeMemory();
long time4 = System.currentTimeMillis();
System.out.println("StringBuilder 占用内存 : " + (num3 - num4));
System.out.println("StringBuilder 占用时间 : " + (time4 - time3));
}
}
执行结果如图所示:
时间处理相关类
“时间如流水,一去不复返”,时间是一维的。所以,我们需要一把刻度尺来表达和度量时间。在计算机世界,我们把 1970 年 1 月 1 日 00:00:00 定为基准时间,每个度量单位是毫秒(1 秒的千分之一),如图所示。
我们用 long 类型的变量来表示时间,从基准时间前后几亿年都能表示。
这个“时刻数值”是所有时间类的核心值,年月日都是根据这个“数值”计算出来的。
我们工作学习涉及的时间相关类有如下这些:
Date 时间类(java.util.Date)
在标准 Java 类库中包含一个 Date 类。它的对象表示一个特定的瞬间,精确到毫秒。
- Date() 分配一个 Date 对象,并初始化此对象为系统当前的日期和时间,可以精确到毫秒)。
- Date(long date) 分配 Date 对象并初始化此对象,以表示自从标准基准时间以来的毫秒数。
- boolean equals(Object obj) 比较两个日期的相等性。
- long getTime() 返回毫秒数。
- String toString() 把此 Date 对象转换为以下形式的 String:dow mon dd hh:mm:ss zzz yyyy 其中:dow 是一周中的某一天 。
【示例】Date 类的使用
long nowNum = System.currentTimeMillis(); //当前时刻对应的毫秒数
Date d = new Date(); //当前时刻的对象
System.out.println(d.getTime()); //返回时间对应的毫秒数
Date d2 = new Date(1000L * 3600 * 24 * 365 * 150); //距离 1970年 150 年
System.out.println(d2);
DateFormat 类和 SimpleDateFormat 类
·DateFormat 类的作用
把时间对象转化成指定格式的字符串。反之,把指定格式的字符串转化成时间对象。
DateFormat 是一个抽象类,一般使用它的的子类SimpleDateFormat 类来实现。
【示例】DateFormat 类和 SimpleDateFormat 类的使用
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class TestDateFormat {
public static void main(String[ ] args) throws ParseException {
// new 出 SimpleDateFormat 对象
SimpleDateFormat s1 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
SimpleDateFormat s2 = new SimpleDateFormat("yyyy-MM-dd");
// 将时间对象转换成字符串
String daytime = s1.format(new Date());
System.out.println(daytime);
System.out.println(s2.format(new Date()));
System.out.println(new SimpleDateFormat("hh:mm:ss").format(new Date()));
// 将符合指定格式的字符串转成成时间对象.字符串格式需要和指定格式一致。
String time = "2049-10-1";
Date date = s2.parse(time);
System.out.println("date1: " + date);
time = "2049-10-1 20:15:30";
date = s1.parse(time);
System.out.println("date2: " + date);
}
}
代码中的格式化字符的具体含义见表:
格式化字符的含义
时间格式字符也可以为我们提供其他的便利。比如:获得当前时间是今年的第几天。
【示例】获取今天时本年度第几天
import java.text.SimpleDateFormat;
import java.util.Date;
public class TestDateFormat2 {
public static void main(String[ ] args) {
SimpleDateFormat s1 = new SimpleDateFormat("D");
String daytime = s1.format(new Date());
System.out.println(daytime);
}
}
执行结果如图所示:
Calendar 日历类
Calendar 类是一个抽象类,为我们提供了关于日期计算的功能,比如:年、月、日、时、分、秒的展示和计算。
GregorianCalendar 是 Calendar 的子类,表示公历。
菜鸟雷区
注意月份的表示,一月是 0,二月是 1,以此类推,12 月是 11。 因为大多数人习惯于使用单词而不是使用数字来表示月份,这样程序也许更易读,父类 Calendar 使用常量来表示月份:JANUARY、FEBRUARY 等等。
【示例】GregorianCalendar 类和 Calendar 类的使用
import java.util.*;
public class TestCalendar {
public static void main(String[ ] args) {
// 得到相关日期元素
GregorianCalendar calendar = new GregorianCalendar(2049, 9, 1, 22, 10, 50);
int year = calendar.get(Calendar.YEAR); // 打印:2049
int month = calendar.get(Calendar.MONTH); // 打印:9
int day = calendar.get(Calendar.DAY_OF_MONTH); // 打印:1
int day2 = calendar.get(Calendar.DATE); // 打印:1
// 日:Calendar.DATE 和 Calendar.DAY_OF_MONTH 同义
int date = calendar.get(Calendar.DAY_OF_WEEK); // 打印:1
// 星期几 这里是:1-7.周日是 1,周一是 2,。。。周六是 7
System.out.println(year);
System.out.println(month);
System.out.println(day);
System.out.println(day2);
System.out.println(date);
// 设置日期
GregorianCalendar calendar2 = new GregorianCalendar();
calendar2.set(Calendar.YEAR, 2049);
calendar2.set(Calendar.MONTH, Calendar.OCTOBER); // 月份数:0-11
calendar2.set(Calendar.DATE, 1);
calendar2.set(Calendar.HOUR_OF_DAY, 10);
calendar2.set(Calendar.MINUTE, 20);
calendar2.set(Calendar.SECOND, 23);
printCalendar(calendar2);
// 日期计算
GregorianCalendar calendar3 = new GregorianCalendar(2049, 9, 1, 22, 10, 50);
calendar3.add(Calendar.MONTH, -7); // 月份减 7
calendar3.add(Calendar.DATE, 7); // 增加 7 天
printCalendar(calendar3);
// 日历对象和时间对象转化
Date d = calendar3.getTime();
GregorianCalendar calendar4 = new GregorianCalendar();
calendar4.setTime(new Date());
}
static void printCalendar(Calendar calendar) {
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1;
int day = calendar.get(Calendar.DAY_OF_MONTH);
int date = calendar.get(Calendar.DAY_OF_WEEK) - 1; // 星期几
String week = "" + ((date == 0) ? "日" : date);
int hour = calendar.get(Calendar.HOUR);
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
System.out.printf("%d 年%d 月%d 日,星期%s %d:%d:%d\n", year, month, day,
week, hour, minute, second);
}
}
其他常用类
Math 类
java.lang.Math 提供了一系列静态方法用于科学计算;常用方法如下:
abs 绝对值
acos,asin,atan,cos,sin,tan 三角函数
sqrt 平方根
pow(double a, double b) a 的 b 次幂
max(double a, double b) 取大值
min(double a, double b) 取小值
ceil(double a) 大于 a 的最小整数
floor(double a) 小于 a 的最大整数
random() 返回 0.0 到 1.0 的随机数
long round(double a) double 型的数据 a 转换为 long 型(四舍五入)
toDegrees(double angrad) 弧度->角度
toRadians(double angdeg) 角度->弧度
【示例】Math 类的常用方法
public class TestMath {
public static void main(String[ ] args) {
//取整相关操作
System.out.println(Math.ceil(3.2));
System.out.println(Math.floor(3.2));
System.out.println(Math.round(3.2));
System.out.println(Math.round(3.8));
//绝对值、开方、a 的 b 次幂等操作
System.out.println(Math.abs(-45));
System.out.println(Math.sqrt(64));
System.out.println(Math.pow(5, 2));
System.out.println(Math.pow(2, 5));
//Math 类中常用的常量
System.out.println(Math.PI);
System.out.println(Math.E);
//随机数
System.out.println(Math.random());// [0,1)
}
}
执行结果如图所示:
Random 类
Random 类: 专门用来生成随机数。
import java.util.Random;
public class TestRandom {
public static void main(String[ ] args) {
Random rand = new Random();
//随机生成[0,1)之间的 double 类型的数据
System.out.println(rand.nextDouble());
//随机生成 int 类型允许范围之内的整型数据
System.out.println(rand.nextInt());
//随机生成[0,1)之间的 float 类型的数据
System.out.println(rand.nextFloat());
//随机生成 false 或者 true
System.out.println(rand.nextBoolean());
//随机生成[0,10)之间的 int 类型的数据
System.out.print(rand.nextInt(10));
//随机生成[20,30)之间的 int 类型的数据
System.out.print(20 + rand.nextInt(10));
}
}
注意
Random 类位于 java.util 包下。
File 类
File 类的基本用法
java.io.File 类:代表文件和目录,用于:读取文件、创建文件、删除文件、修改文件。
【示例】使用 File 类创建文件
File 类的常见构造方法:public File(String pathname)
以 pathname 为路径创建 File 对象,如果 pathname 是相对路径,则默认的当前路径在系统属性 user.dir
中存储。
import java.io.File;
public class TestFile1 {
public static void main(String[ ] args) throws Exception {
System.out.println(System.getProperty("user.dir"));
File f = new File("a.txt"); //相对路径:默认放到 user.dir 目录下面
f.createNewFile();//创建文件
File f2 = new File("d:/b.txt");//绝对路径
f2.createNewFile();
}
}
user.dir 就是本项目的目录。上面代码执行后,在本项目和 D 盘下都生成了新的文件。
通过 File 对象可以访问文件的属性:
【示例】使用 File 类访问文件或目录属性
import java.io.File;
import java.util.Date;
public class TestFile2 {
public static void main(String[ ] args) throws Exception {
File f = new File("d:/b.txt");
System.out.println("File 是否存在:"+f.exists());
System.out.println("File 是否是目录:"+f.isDirectory());
System.out.println("File 是否是文件:"+f.isFile());
System.out.println("File 最后修改时间:"+new Date(f.lastModified()));
System.out.println("File 的大小:"+f.length());
System.out.println("File 的文件名:"+f.getName());
System.out.println("File 的目录路径:"+f.getAbsolutePath());
}
}
执行结果如图所示:
通过 File 对象创建空文件或目录(在该对象所指的文件或目录不存在的情况下)
【示例】使用 mkdir 创建目录
import java.io.File;
public class TestFile3 {
public static void main(String[ ] args) throws Exception {
File f = new File("d:/c.txt");
f.createNewFile(); // 会在 d 盘下面生成 c.txt 文件
f.delete(); // 将该文件或目录从硬盘上删除
File f2 = new File("d:/电影/华语/大陆");
boolean flag = f2.mkdir(); //目录结构中有一个不存在,则不会创建整个目录树
System.out.println(flag);//创建失败
}
}
【示例】使用 mkdirs 创建目录
import java.io.File;
public class TestFile4 {
public static void main(String[ ] args) throws Exception {
File f = new File("d:/c.txt");
f.createNewFile(); // 会在 d 盘下面生成 c.txt 文件
f.delete(); // 将该文件或目录从硬盘上删除
File f2 = new File("d:/电影/华语/大陆");
boolean flag = f2.mkdirs();//目录结构中有一个不存在也没关系;创建整个目录树
System.out.println(flag);//创建成功
}
}
递归遍历目录结构和树状展现
【示例】使用递归算法,以树状结构展示目录树
import java.io.File;
public class TestFile6 {
public static void main(String[ ] args) {
File f = new File("d:/电影");
printFile(f, 0);
}
/**
* 打印文件信息
* @param file 文件名称
* @param level 层次数(实际就是:第几次递归调用)
*/
static void printFile(File file, int level) {
//输出层次数
for (int i = 0; i < level; i++) {
System.out.print("-");
}
//输出文件名
System.out.println(file.getName());
//如果 file 是目录,则获取子文件列表,并对每个子文件进行相同的操作
if (file.isDirectory()) {
File[ ] files = file.listFiles();
for (File temp : files) {
//递归调用该方法:注意要+1
printFile(temp, level + 1);
}
}
}
}
执行结果如图所示:
枚举
JDK1.5 引入了枚举类型。枚举类型的定义包括枚举声明和枚举体。格式如下:
enum 枚举名 {
枚举体(常量列表)
}
枚举体就是放置一些常量。我们可以写出我们的第一个枚举类型,如示例所示:
【示例】创建枚举类型
enum Season {
SPRING, SUMMER, AUTUMN, WINTER
}
所有的枚举类型隐性地继承自 java.lang.Enum
。枚举实质上还是类!而每个被枚举的成员实质就是一个枚举类型的实例,他们默认都是 public static final
修饰的。可以直接通过枚举类型名使用它们。
老鸟建议
当你需要定义一组常量时,可以使用枚举类型。
尽量不要使用枚举的高级特性,事实上高级特性都可以使用普通类来实现,没有必要引入枚举,增加程序的复杂性!
【示例】枚举的使用
import java.util.Random;
public class TestEnum {
public static void main(String[ ] args) {
// 枚举遍历
for (Week k : Week.values()) {
System.out.println(k);
}
// switch 语句中使用枚举
int a = new Random().nextInt(4); // 生成 0,1,2,3 的随机数
switch (Season.values()[a]) {
case SPRING:
System.out.println("春天");
break;
case SUMMER:
System.out.println("夏天");
break;
case AUTUMN:
System.out.println("秋天");
break;
case WINTER:
System.out.println("冬天");
break;
}
}
}
/**季节*/
enum Season {
SPRING, SUMMER, AUTUMN, WINTER
}
/**星期*/
enum Week {
星期一, 星期二, 星期三, 星期四, 星期五, 星期六, 星期日
}
容器
一、 泛型(Generics)
1 泛型简介
1.1泛型基本概念
泛型是 JDK1.5 以后增加的,它可以帮助我们建立类型安全的集合。
泛型的本质就是“数据类型的参数化”,处理的数据类型不是固定的,而是可以作为参数传入。 我们可以把“泛型”理解为数据类型的一个占位符(类似:形式参数),即告诉编译器,在调用泛型时必须传入实际类型。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
1.2泛型的好处
在不使用泛型的情况下,我们可以使用 Object 类型来实现任意的参数类型,但是在使用时需要我们强制进行类型转换。这就要求程序员明确知道实际类型,不然可能引起类型转换错误;但是,在编译期我们无法识别这种错误,只能在运行期发现这种错误。使用泛型的好处就是可以在编译期就识别出这种错误,有了更好的安全性;同时,所有类型转换由编译器完成,在程序员看来都是自动转换的,提高了代码的可读性。
总结一下,就是使用泛型主要是两个好处:
- 代码可读性更好【不用强制转换】
- 程序更加安全【只要编译时期没有警告,运行时期就不会出现 ClassCastException 异常】
1.3类型擦除
编码时采用泛型写的类型参数,编译器会在编译时去掉,这称之为“类型擦除”。
泛型主要用于编译阶段,编译后生成的字节码 class 文件不包含泛型中的类型信息,涉及类型转换仍然是普通的强制类型转换。 类型参数在编译后会被替换成 Object,运行时虚拟机并不知道泛型。
泛型主要是方便了程序员的代码编写,以及更好的安全性检测。
2 泛型的使用
2.1定义泛型
泛型字符可以是任何标识符,一般采用几个标记:E、T、K、V、N、?。
2.2泛型类
泛型类就是把泛型定义在类上,用户使用该类的时候,才把类型明确下来。泛型类的具体使用方法是在类的名称后添加一个或多个类型参数声明,如:<T>、<T,K,V>
2.2.1 语法结构
public class 类名<泛型表示符号> {
}
2.2.2 示例
public class Generic<T> {
private T flag;
public void setFlag(T flag){
this.flag = flag;
}
public T getFlag(){
return this.flag;
}
}
public class Test {
public static void main(String[] args) {
Generic<String> generic = new Generic<>();
generic.setFlag("admin");
String flag = generic.getFlag();
System.out.println(flag);
Generic<Integer> generic1 = new Generic<>();
generic1.setFlag(100);
Integer flag1 = generic1.getFlag();
System.out.println(flag1);
}
}
2.3泛型接口
泛型接口和泛型类的声明方式一致。泛型接口的具体类型需要在实现类中进行声明。
2.3.1 语法结构
public interface 接口名<泛型表示符号> {
}
2.3.2 示例
public interface Igeneric<T> {
T getName(T name);
}
public class IgenericImpl implements Igeneric<String> {
@Override
public String getName(String name) {
return name;
}
}
public class Test2 {
public static void main(String[] args) {
IgenericImpl igeneric= new IgenericImpl();
String name = igeneric.getName("oldlu");
System.out.println(name);
Igeneric<String> igeneric1 = new IgenericImpl();
String name1 = igeneric1.getName("bjsxt");
System.out.println(name1);
}
}
2.4泛型方法
泛型类中所定义的泛型,在方法中也可以使用。但是,我们经常需要仅仅在某一个方法上使用泛型,这时候可以使用泛型方法。
泛型方法是指将方法的参数类型定义成泛型,以便在调用时接收不同类型的参数。类型参数可以有多个,用逗号隔开,如:<K,V>。定义时,类型参数一般放到返回值前面。
调用泛型方法时,不需要像泛型类那样告诉编译器是什么类型,编译器可以自动推断出类型来。
2.4.1 非静态方法
2.4.1.1 语法结
public <泛型表示符号> void getName(泛型表示符号 name){
}
public <泛型表示符号> 泛型表示符号 getName(泛型表示符号 name){
}
2.4.1.2 示例
public class MethodGeneric {
public <T> void setName(T name){
System.out.println(name);
}
public <T> T getName(T name){
return name;
}
}
public class Test3 {
public static void main(String[] args) {
MethodGeneric methodGeneric = new MethodGeneric();
methodGeneric.setName("oldlu");
methodGeneric.setName(123123);
MethodGeneric methodGeneric2 = new MethodGeneric();
String name = methodGeneric2.getName("Bjsxt");
Integer name1 = methodGeneric2.getName(123);
System.out.println(name1);
System.out.println(name);
}
2.4.2 静态方法
静态方法中使用泛型时有一种情况需要注意一下,那就是静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。
2.4.2.1 语法结构
public static <泛型表示符号> void setName(泛型表示符号 name){
}
public static <泛型表示符号> 泛型表示符号 getName(泛型表示符号 name){
}
2.4.2.2 示例
public class MethodGeneric {
public static <T> void setFlag(T flag){
System.out.println(flag);
}
public static <T> T getFlag(T flag){
return flag;
}
}
public class Test4 {
public static void main(String[] args) {
MethodGeneric.setFlag("oldlu");
MethodGeneric.setFlag(123123);
String flag = MethodGeneric.getFlag("bjsxt");
System.out.println(flag);
Integer flag1 = MethodGeneric.getFlag(123123);
System.out.println(flag1);
}
}
2.4.3 泛型方法与可变参数
在泛型方法中,泛型也可以定义可变参数类型。
2.4.3.1 语法结构
public <泛型表示符号> void showMsg(泛型表示符号... agrs){
}
2.4.3.2 示例
public class MethodGeneric {
public <T> void method(T...args){
for(T t:args){
System.out.println(t);
}
}
}
public class Test5 {
public static void main(String[] args) {
MethodGeneric methodGeneric = new MethodGeneric();
String[] arr = new String[]{"a","b","c"};
Integer[] arr2 = new Integer[]{1,2,3};
methodGeneric.method(arr);
methodGeneric.method(arr2);
}
}
2.5通配符和上下限定
2.5.1 无界通配符
“?
”表示类型通配符,用于代替具体的类型。它只能在“<>
”中使用。可以解决当具体类型不确定的问题。
2.5.1.1 语法结构
public void showFlag(Generic<?> generic){
}
2.5.1.2 示例
public class Generic<T> {
private T flag;
public void setFlag(T flag){
this.flag = flag;
}
public T getFlag(){
return this.flag;
}
}
public class ShowMsg {
public void showFlag(Generic<?> generic){
System.out.println(generic.getFlag());
}
}
public class Test6 {
public static void main(String[] args) {
ShowMsg showMsg = new ShowMsg();
Generic<Integer> generic = new Generic<>();
generic.setFlag(20);
showMsg.showFlag(generic);
Generic<Number> generic1 = new Generic<>();
generic1.setFlag(50);
showMsg.showFlag(generic1);
Generic<String> generic2 = new Generic<>();
generic2.setFlag("oldlu");
showMsg.showFlag(generic2);
}
}
2.5.2 统配符的上限限定
上限限定表示通配符的类型是 T 类以及 T 类的子类或者 T 接口以及 T 接口的子接口。该方式同样适用于与泛型的上限限定。
2.5.2.1 语法结构
public void showFlag(Generic<? extends Number> generic){
}
2.5.2.2 示例
public class ShowMsg {
public void showFlag(Generic<? extends Number> generic){
System.out.println(generic.getFlag());
}
}
public class Test6{
public static void main(String[] args) {
ShowMsg showMsg = new ShowMsg();
Generic<Integer> generic = new Generic<>();
generic.setFlag(20);
showMsg.showFlag(generic);
Generic<Number> generic1 = new Generic<>();
generic1.setFlag(50);
showMsg.showFlag(generic1);
}
]
2.5.3 通配符的下限限定
下限限定表示通配符的类型是 T 类以及 T 类的父类或者 T 接口以及 T 接口的父接口。
注意:该方法不适用泛型类。
2.5.3.1 语法结构
public void showFlag(Generic<? super Integer> generic){
}
2.5.3.2 示例
public class ShowMsg {
public void showFlag(Generic<? super Integer> generic){
System.out.println(generic.getFlag());
}
}
public class Test6 {
public static void main(String[] args) {
ShowMsg showMsg = new ShowMsg();
Generic<Integer> generic = new Generic<>();
generic.setFlag(20);
showMsg.showFlag(generic);
Generic<Number> generic1 = new Generic<>();
generic1.setFlag(50);
showMsg.showFlag(generic1);
}
}
3 泛型总结
泛型主要用于编译阶段,编译后生成的字节码 class 文件不包含泛型中的类型信息。 类型参数在编译后会被替换成 Object,运行时虚拟机并不知道泛型。因此,使用泛型时,如下几种情况是错误的:
- 基本类型不能用于泛型。
Test<int> t;
这样写法是错误,我们可以使用对应的包装类;Test<Integer> t ;
- 不能通过类型参数创建对象。
T elm = new T();
运行时类型参数 T 会被替换成 Object,无法创建 T 类型的对象,容易引起误解,所以在 Java中不支持这种写法。
二、 容器
1 容器简介
容器,是用来容纳物体、管理物体。生活中,我们会用到各种各样的容器。如锅碗瓢盆、箱子和包等。
程序中的“容器”也有类似的功能,用来容纳和管理数据。比如,如下新闻网站的新闻列表、教育网站的课程列表就是用“容器”来管理:
视频课程信息也是使用“容器”来管理:
开发和学习中需要时刻和数据打交道,如何组织这些数据是我们编程中重要的内容。我们一般通过“容器”来容纳和管理数据。事实上,我们前面所学的数组就是一种容器,可以在其中放置对象或基本类型数据。
数组的优势:是一种简单的线性序列,可以快速地访问数组元素,效率高。如果从效率和类型检查的角度讲,数组是最好的。
数组的劣势:不灵活。容量需要事先定义好,不能随着需求的变化而扩容。比如:我们在一个应付管理系统中,要把今天注册的所有用户取出来,那么这样的用户有多少个?我们在写程序时无法确定的。因此,在这里就不能使用数组。
基于数组并不能满足我们对于“管理和组织数据的需求”,所以我们需要一种更强大、更灵活、容量随时可扩的容器来装载我们的对象。这就是我们要学习的容器。容器(Collection)也称之为集合。
2 容器的结构
2.1结构图
2.1.1 单例集合
单例集合:将数据一个一个的进行存储。
2.1.2 双例集合
双例集合:基于 Key 与 Value的结构存储数据。
3 单利集合的使用
3.1Collection 接口介绍
Collection 是单例集合根接口,它是集中、收集的意思。Collection 接口的两个子接口是 List、Set 接口。
3.2Collection 接口中的抽象方法
由于 List、Set 是 Collection 的子接口,意味着所有 List、Set 的实现类都有上面的方法。我们下一节中,通过 ArrayList 实现类来测试上面的方法。
JDK8 之后,Collection 接口新增的方法(将在 JDK 新特性和函数式编程中介绍
3.3List 接口介绍
3.3.1 List 接口特点
有序:有序(元素存入集合的顺序和取出的顺序一致)。List 中每个元素都有索引标记。可以根据元素的索引标记(在 List 中的位置)访问元素,从而精确控制这些元素。
可重复:List 允许加入重复的元素。更确切地讲,List 通常允许满足 e1.equals(e2) 的元素重复加入容器。
3.3.2 List的常用方法
除了 Collection 接口中的方法,List 多了一些跟顺序(索引)有关的方法,参见下表:
3.4ArrayList 容器类
ArrayList 是 List 接口的实现类。是 List 存储特征的具体实现。
ArrayList 底层是用数组实现的存储。 特点:查询效率高,增删效率低,线程不安全。
3.4.1 添加元素
public class ArrayListTest {
public static void main(String[] args) {
//实例化 ArrayList 容器
List<String> list = new ArrayList<>();
//添加元素
boolean flag = list.add("bjsxt");
boolean flag2 = list.add("itbz");
System.out.println(flag);
//索引的数值不能大于元素的个数。
list.add(3,"Oldlu");
}
}
3.4.2 获取元素
//通过指定索引位置获取元素
System.out.println(list.get(0));
System.out.println(list.get(1));
System.out.println(list.get(2));
System.out.println("---------------");
//通过循环获取集合中所用元素
//size():返回集合中元素个数
for(int i=0;i<list.size();i++){
System.out.println(list.get(i));
}
3.4.3 删除元素
3.4.3.1 根据索引删除元素
//根据索引位置删除元素
String value = list.remove(1);
System.out.println(value);
for(int i=0;i<list.size();i++){
System.out.println(list.get(i));
}
3.4.3.2 删除指定元素
//删除指定元素
boolean flag3 = list.remove("itbz");
System.out.println(flag3);
for(int i=0;i<list.size();i++){
System.out.println(list.get(i));
}
3.4.4 替换元素
String val = list.set(0, "itbz");
System.out.println(val);
for(int i=0;i<list.size();i++){
System.out.println(list.get(i));
}
3.4.5 清空容器
list.clear();
System.out.println(list.size());
3.4.6 判断容器是否为空
//如果容器为空则返回 true,否则返回 false
boolean flag4 = list.isEmpty();
System.out.println(flag4);
3.4.7 判断容器中是否包含指定元素
//如果在容器中包含指定元素则返回 true,否则返回 false。
boolean flag5 = list.contains("oldlu1");
System.out.println(flag5);
3.4.8 查找元素的位置
3.4.8.1 查找元素第一次出现的位置
//indexOf 方法返回的是元素在容器中第一次出现的位置。
//在容器中不存在则返回-1
int index = list.indexOf("itbz4");
System.out.println(index);
3.4.8.2 查找元素最后一次出现的位置
//lastIndexOf 方法返回的是元素在容器中最后一次出现的位置,如果元素
//在容器中不存在则返回-1
int lastIndex = list.lastIndexOf("itbz");
System.out.println(lastIndex);
3.4.9 将单例集合转换成数组
3.4.9.1 转换为 Object 数组
//将 ArrayList 转换为 Object[]。
//但是不能将转换的数组做强制类型转换。
Object[] arr = list.toArray();
for(int i=0;i<arr.length;i++){
String str = (String)arr[i];
System.out.println(str);
}
3.4.9.2 转换泛型类型数组
//可以将单例集合转换为指定类型数组。
//但是。类型需要参考泛型中的类型。
String[] arr2 = list.toArray(new String[list.size()]);
for(int i=0;i<arr2.length;i++){
System.out.println(arr2[i]);
}
3.4.10 容器的并集操作
//容器的并集操作
List<String> a = new ArrayList<>();
a.add("a");
a.add("b");
a.add("c");
List<String> b = new ArrayList<>();
b.add("b");
b.add("c");
b.add("d");
//a 并 b
boolean flag6 = a.addAll(b);
System.out.println(flag6);
for(String str:a){
System.out.println(str);
}
3.4.11 容器的交集操作
//容器的交集操作
List<String> a1 = new ArrayList<>();
a1.add("a");
a1.add("b");
a1.add("c");
List<String> b1 = new ArrayList<>();
b1.add("b");
b1.add("c");
b1.add("d");
boolean flag7 = a1.retainAll(b1);
System.out.println(flag7);
for(String str :a1){
System.out.println(str);
}
3.4.12 容器的差集操作
//容器的差集操作
List<String> a2 = new ArrayList<>();
a2.add("a");
a2.add("b");
a2.add("c");
List<String> b2 = new ArrayList<>();
b2.add("b");
b2.add("c");
b2.add("d");
boolean flag8 = a2.removeAll(b2);
System.out.println(flag8);
for(String str :a2){
System.out.println(str);
}
3.4.13 ArrayList 源码分析
3.4.13.1 ArrayList 底层存储方式
ArrayList 底层是用数组实现的存储。
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* The array buffer into which the elements of the ArrayList are stored.
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer.
Any
* empty ArrayList with elementData ==
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested
class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
3.4.13.2 初始容量
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 1
3.4.13.3 添加元素
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
/**
* This helper method split out from add(E) to keep method
* bytecode size under 35 (the -XX:MaxInlineSize default
value),
* which helps when add(E) is called in a C1-compiled loop.
*/
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
3.4.13.4 数组扩容
//容量检查
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData,
minCapacity));
}
//容量确认
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
//判断是否需要扩容,数组中的元素个数-数组长度,如果大于 0 表明需要扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//扩容 1.5 倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
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);
}
3.5Vector 容器类
Vector 底层是用数组实现的,相关的方法都加了同步检查,因此“线程安全,效率低”。比如,indexOf 方法就增加了 synchronized 同步标记。
3.5.1 Vector 的使用
Vector 的使用与 ArrayList 是相同的,因为他们都实现了 List 接口,对 List 接口中的抽象方法做了具体实现。
public class VectorTest {
public static void main(String[] args){
//实例化 Vector
List<String> v = new Vector<>();
v.add("a");
v.add("b");
v.add("a");
for(int i=0;i<v.size();i++){
System.out.println(v.get(i));
}
System.out.println("----------------------");
for(String str:v){
System.out.println(str);
}
}
}
3.5.2 Vector 源码分析
3.5.2.1 初始化容器
/**
* The array buffer into which the components of the vector are
* stored. The capacity of the vector is the length of this array buffer,
* and is at least large enough to contain all the vector's elements.
*
* <p>Any array elements following the last element in the Vector are
null.
*
* @serial
*/
protected Object[] elementData;
public Vector() {
this(10);
}
/**
* Constructs an empty vector with the specified initial capac
* * capacity increment.
*
* @param initialCapacity the initial capacity of the vector
* @param capacityIncrement the amount by which the capacity is
* increased when the vector overflows
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
3.5.2.2 添加元素
/**
* Appends the specified element to the end of this Vector.
*
* @param e element to be appended to this Vector
* @return {@code true} (as specified by {@link Collection#add})
* @since 1.2
*/
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
3.5.2.3 数组扩容
/**
* This implements the unsynchronized semantics of ensureCapacity.
* Synchronized methods in this class can internally call this
* method for ensuring capacity without incurring the cost of an
* extra synchronization.
*
* @see #ensureCapacity(int)
*/
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
//判断是否需要扩容,数组中的元素个数-数组长度,如果大于 0 表明需要扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//扩容 2 倍
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);
}
3.5.3 Stack 容器
3.5.3.1 Stack 容器介绍
Stack 栈容器,是 Vector 的一个子类,它实现了一个标准的后进先出(LIFO:Last In Frist Out)的栈。
3.5.3.1.1 Stack 特点是
后进先出。它通过 5 个操作方法对 Vector 进行扩展,允许将向量视为堆栈。
3.5.3.1.2 操作栈的方法
3.5.3.2 Stack 的使用
public class StackTest {
public static void main(String[] args) {
//实例化栈容器
Stack<String> stack = new Stack<>();
//将元素添加到栈容器中
stack.push("a");
stack.push("b");
stack.push("c");
//判断栈容器是否为空
System.out.println(stack.empty());
//查看栈顶元素
System.out.println(stack.peek());
//返回元素在栈容器中的位置
System.out.println(stack.search("c"));
//获取栈容器中的元素
String p1 = stack.pop();
System.out.println(p1);
String p2 = stack.pop();
System.out.println(p2);
String p3 = stack.pop();
System.out.println(p3);
}
}
3.5.3.3 Stack 的使用案例
判断元素的对称性
String str="...{.....[....(....)...]....}..(....)..[...]...";
//匹配符号的对称性
public void symmetry(){
String str="...{.....[....(....)...]....}..(....)..[...].(.).";
//实例化 Stack
Stack<String> stack = new Stack<>();
//假设修正法
boolean flag = true;//假设是匹配的
//拆分字符串获取字符
for(int i=0;i<str.length();i++){
char c = str.charAt(i);
if(c == '{'){
stack.push("}");
}
if(c == '['){
stack.push("]");
}
if(c == '('){
stack.push(")");
}
//判断符号是否匹配
if(c == '}' || c == ']' || c == ')'){
if(stack.empty()){
//修正处理
flag = false;
break;
}
String x = stack.pop();
if(x.charAt(0) != c){
//修正处理
flag = false;
break;
}
}
}
if(!stack.empty()){
//修正处理
flag = false;
}
System.out.println(flag);
}
3.6LinkedList 容器类
LinkedList 底层用双向链表实现的存储。特点:查询效率低,增删效率高,线程不安全。
双向链表也叫双链表,是链表的一种,它的每个数据节点中都有两个指针,分别指向前一个节点和后一个节点。 所以,从双向链表中的任意一个节点开始,都可以很方便地找到所有节点。
3.6.1 双向链表介
class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}
3.6.2 LinkedList 的使用(List 标准)
LinkedList 实现了 List 接口,所以 LinkedList 是具备 List 的存储特征的(有序,元素有重复)。
public class LinkedListTest {
public static void main(String[] args) {
List<String> list = new LinkedList<>();
//添加元素
list.add("a");
list.add("b");
list.add("c");
list.add("a");
//获取元素
for(int i=0;i<list.size();i++){
System.out.println(list.get(i));
}
System.out.println("-------------------");
for(String str :list){
System.out.println(str);
}
}
}
3.6.3 LinkedList 的使用(非 List 标准)
System.out.println("-------LinkedList-------------");
LinkedList<String> linkedList1 = new LinkedList<>();
linkedList1.addFirst("a");
linkedList1.addFirst("b");
linkedList1.addFirst("c");
for (String str:linkedList1){
System.out.println(str);
}
System.out.println("----------------------");
LinkedList<String> linkedList = new LinkedList<>();
linkedList.addLast("a");
linkedList.addLast("b");
linkedList.addLast("c");
for (String str:linkedList){
System.out.println(str);
}
System.out.println("---------------------------");
System.out.println(linkedList.getFirst());
System.out.println(linkedList.getLast());
System.out.println("-----------------------");
linkedList.removeFirst();
linkedList.removeLast();
for (String str:linkedList){
System.out.println(str);
}
System.out.println("-----------------------");
linkedList.addLast("c");
linkedList.pop();
for (String str:linkedList){
System.out.println(str);
}
System.out.println("-------------------");
linkedList.push("h");
for (String str:linkedList){
System.out.println(str);
}
System.out.println(linkedList.isEmpty());
}
3.6.4 LinkedList 源码分析
3.6.4.1 节点类
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
3.6.4.2 成员变量
transient int size = 0;
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> las
3.6.4.3 添加元素
/**
* Appends the specified element to the end of this list.
*
* <p>This method is equivalent to {@link #addLast}.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
linkLast(e);
return true;
}
/**
* Links e as last element.
*/
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
头尾添加元素
3.6.4.4.1 addFirst
/**
* Inserts the specified element at the beginning of this list.
*
* @param e the element to add
*/
public void addFirst(E e) {
linkFirst(e);
}
/**
* Links e as first element.
*/
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
3.6.4.4.2 addLast
/**
* Appends the specified element to the end of this list.
*
* <p>This method is equivalent to {@link #add}.
*
* @param e the element to add
*/
public void addLast(E e) {
linkLast(e);
}
/**
* Links e as last element.
*/
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
3.6.4.5 在指定位置添加元素
/**
* Inserts the specified element at the specified position in this list.
* Shifts the element currently at that position (if any) and any
* subsequent elements to the right (adds one to their indices).
*
* @param index index at which the specified element is to be inserted
* @param element element to be inserted
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* Links e as last element.
*/
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
/**
* Returns the (non-null) Node at the specified element index.
*/
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
/**
* Inserts element e before non-null Node succ.
*/
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
3.6.4.6 获取元素
/**
* Returns the element at the specified position in this list.
*
* @param index index of the element to return
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* Tells if the argument is the index of an existing element.
*/
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
/**
* Returns the (non-null) Node at the specified element index.
*/
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
3.6.4.7 删除指定位置元素
/**
* Removes the element at the specified position in this list. Shifts
any
* subsequent elements to the left (subtracts one from their indices).
* Returns the element that was removed from the list.
*
* @param index the index of the element to be removed
* @return the element previously at the specified position
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* Tells if the argument is the index of an existing element.
*/
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
/**
* Returns the (non-null) Node at the specified element index.
*/
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
/**
* Unlinks non-null node x.
*/
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
3.7Set 接口介绍
Set 接口继承自 Collection,Set 接口中没有新增方法,方法和 Collection 保持完全一致。我们在前面通过 List 学习的方法,在 Set 中仍然适用。因此,学习 Set 的使用将没有任何难度。
3.7.1 Set 接口特点
Set 特点:无序、不可重复。无序指 Set 中的元素没有索引,我们只能遍历查找;不可重复指不允许加入重复的元素。更确切地讲,新元素如果和 Set 中某个元素通过 equals()
方法对比为 true,则只能保留一个。
Set 常用的实现类有:HashSet、TreeSet 等,我们一般使用 HashSet。
3.7.2 HashSet 容器类
HashSet 是一个没有重复元素的集合,不保证元素的顺序。而且 HashSet 允许有 null 元素。HashSet 是采用哈希算法实现,底层实际是用 HashMap 实现的(HashSet 本质就是一个简化版的 HashMap),因此,查询效率和增删效率都比较高。
3.7.2.1 Hash 算法原理
Hash 算法也称之为散列算法。
3.7.3 HashSet 的使用
public class HashSetTest {
public static void main(String[] args){
//实例化 HashSet
Set<String> set = new HashSet<>();
//添加元素
set.add("a");
set.add("b1");
set.add("c2");
set.add("d");
set.add("a");
//获取元素,在 Set 容器中没有索引,所以没有对应的 get(int index)方法
for(String str: set){
System.out.println(str);
}
System.out.println("--------------------");
//删除元素
boolean flag = set.remove("c2");
System.out.println(flag);
for(String str: set){
System.out.println(str);
}
System.out.println("------------------------");
int size = set.size();
System.out.println(size);
}
}
3.7.4 HashSet 存储特征分析
HashSet 是一个不保证元素的顺序且没有重复元素的集合,是线程不安全的。HashSet允许有 null 元素。
无序:
在 HashSet 中底层是使用 HashMap 存储元素的。HashMap 底层使用的是数组与链表实现元素的存储。元素在数组中存放时,并不是有序存放的也不是随机存放的,而是对元素的哈希值进行运算决定元素在数组中的位置。
不重复:
当两个元素的哈希值进行运算后得到相同的在数组中的位置时,会调用元素的 equals()
方法判断两个元素是否相同。如果元素相同则不会添加该元素,如果不相同则会使用单向链表保存该元素。
3.7.5 通过 HashSet 存储自定义对象
3.7.5.1 创建 Users 对象
public class Users {
private String username;
private int userage;
public Users(String username, int userage) {
this.username = username;
this.userage = userage;
}
public Users() {
}
@Override
public boolean equals(Object o) {
System.out.println("equals...");
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Users users = (Users) o;
if (userage != users.userage) return false;
return username != null ? username.equals(users.username) : users.username == null;
}
@Override
public int hashCode() {
int result = username != null ? username.hashCode() : 0;
result = 31 * result + userage;
return result;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getUserage() {
return userage;
}
public void setUserage(int userage) {
this.userage = userage;
}
@Override
public String toString() {
return "Users{" +
"username='" + username + '\'' +
", userage=" + userage +
'}';
}
}
3.7.5.2 在 HashSet 中存储 Users 对象
//实例化 HashSet
Set<Users> set1 = new HashSet<>();
Users u = new Users("oldlu",18);
Users u1 = new Users("oldlu",18);
set1.add(u);
set1.add(u1);
System.out.println(u.hashCode());
System.out.println(u1.hashCode());
for(Users users:set1){
System.out.println(users);
}
3.7.6 HashSet 底层源码分析
3.7.6.1 成员变量
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Objec
3.7.6.2 添加元素
/**
* Adds the specified element to this set if it is not already present.
* More formally, adds the specified element <tt>e</tt> to this set
if
* this set contains no element <tt>e2</tt> such that
*
<tt>(e==null ? e2==null : e.equals(e2))</tt>.
* If this set already contains the element, the call leaves the set
* unchanged and returns <tt>false</tt>.
*
* @param e element to be added to this set
* @return <tt>true</tt> if this set did not already contain the
specified
* element
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
3.7.7 TreeSet 容器类
TreeSet 是一个可以对元素进行排序的容器。底层实际是用 TreeMap 实现的,内部维持了一个简化版的TreeMap,通过key来存储Set的元素。TreeSet内部需要对存储的元素进行排序,因此,我们需要给定排序规则。
排序规则实现方式:
- 通过元素自身实现比较规则。
- 通过比较器指定比较规则。
3.7.7.1 TreeSet的使用
public class TreeSetTest {
public static void main(String[] args) {
//实例化 TreeSet
Set<String> set = new TreeSet<>();
//添加元素
set.add("c");
set.add("a");
set.add("d");
set.add("b");
set.add("a");
//获取元素
for(String str :set){
System.out.println(str);
}
}
}
3.7.8 通过元素自身实现比较规则
在元素自身实现比较规则时,需要实现Comparable接口中的compareTo 方法,该方法中用来定义比较规则。TreeSet 通过调用该方法来完成对元素的排序处理。
3.7.8.1 创建 Users类
public class Users implements Comparable<Users>{
private String username;
private int userage;
public Users(String username, int userage) {
this.username = username;
this.userage = userage;
}
public Users() {
}
@Override
public boolean equals(Object o) {
System.out.println("equals...");
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Users users = (Users) o;
if (userage != users.userage) return false;
return username != null ? username.equals(users.username) : users.username == null;
}
@Override
public int hashCode() {
int result = username != null ? username.hashCode() : 0;
result = 31 * result + userage;
return result;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getUserage() {
return userage;
}
public void setUserage(int userage) {
this.userage = userage;
}
@Override
public String toString() {
return "Users{" +
"username='" + username + '\'' +
", userage=" + userage +
'}';
}
//定义比较规则
//正数:大,负数:小,0:相等
@Override
public int compareTo(Users o) {
if(this.userage > o.getUserage()){
return 1;
}
if(this.userage == o.getUserage()){
return this.username.compareTo(o.getUsername());
}
return -1;
}
}
3.7.8.2 在 TreeSet 中存放 User对象
Set<Users> set1 = new TreeSet<>();
Users u = new Users("oldlu",18);
Users u1 = new Users("admin",22);
Users u2 = new Users("sxt",22);
set1.add(u);
set1.add(u1);
set1.add(u2);
for(Users users:set1){
System.out.println(users);
}
3.7.9 通过比较器实现比较规则
通过比较器定义比较规则时,我们需要单独创建一个比较器,比较器需要实现Comparator 接口中的 compare 方法来定义比较规则。在实例化 TreeSet 时将比较器对象交给TreeSet 来完成元素的排序处理。此时元素自身就不需要实现比较规则了。
3.7.9.1 创建比较器
public class StudentComparator implements Comparator<Student> {
//定义比较规则
@Override
public int compare(Student o1, Student o2) {
if(o1.getAge() > o2.getAge()){
return 1;
}
if(o1.getAge() == o2.getAge()){
return o1.getName().compareTo(o2.getName());
}
return -1;
}
}
3.7.9.2 创建 Student类
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public Student() {
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
if (age != student.age) return false;
return name != null ? name.equals(student.name) : student.name == null;
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
}
3.7.9.3 在 TreeSet 中存储 Users 对象
Set<Student> set2 = new TreeSet<>(new StudentComparator());
Student s = new Student("oldlu",18);
Student s1 = new Student("admin",22);
Student s2 = new Student("sxt",22);
set2.add(s);
set2.add(s1);
set2.add(s2);
for(Student student:set2){
System.out.println(student);
}
3.7.10 TreeSet 底层源码分析
3.7.10.1 成员变量
/**
* The backing map.
*/
private transient NavigableMap<E,Object> m;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
public TreeSet() {
this(new TreeMap<E,Object>());
}
3.7.10.2 添加元素
/**
* Adds the specified element to this set if it is not already present.
* More formally, adds the specified element <tt>e</tt> to this set
if
* this set contains no element <tt>e2</tt> such that
*
<tt>(e==null ? e2==null : e.equals(e2))</tt>.
* If this set already contains the element, the call leaves the set
* unchanged and returns <tt>false</tt>.
*
* @param e element to be added to this set
* @return <tt>true</tt> if this set did not already contain the
specified
* element
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
3.8单例集合使用案例
需求:
产生 1-10 之间的随机数([1,10]闭区间),将不重复的 10 个随机数放到容器中。
3.8.1 使用 List 类型容器实现
public class ListDemo {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
while(true){
//产生随机数
int num = (int)(Math.random()*10+1);
//判断当前元素在容器中是否存在
if(!list.contains(num)){
list.add(num);
}
//结束循环
if(list.size() == 10){
break;
}
}
for(Integer i:list){
System.out.println(i);
}
}
}
3.8.2 使用 Set 类型容器实现
public class SetDemo {
public static void main(String[] args) {
Set<Integer> set = new HashSet<>();
while(true){
int num = (int)(Math.random()*10+1);
//将元素添加容器中,由于 Set 类型容器是不允许有重复元素的,所以不需要判断。
set.add(num);
//结束循环
if(set.size() == 10){
break;
}
}
for(Integer i:set){
System.out.println(i);
}
}
}
4 双例集合
4.1Map 接口介绍
4.1.1 Map 接口特点
Map 接口定义了双例集合的存储特征,它并不是 Collection 接口的子接口。双例集合的存储特征是以 key 与 value 结构为单位进行存储。体现的是数学中的函数 y=f(x)感念。
Map 与 Collecton 的区别:
- Collection中的容器,元素是孤立存在的(理解为单身),向集合中存储元素采用一个元素的方式存储。
- Map中的容器,元素是成对存在的(理解为现代社会的夫妻),每个元素由键与值两部分组成,通过键可以找对应的值。Collection中容器称为单例集合,Map中的容器称为双列集合。
- Map中的集合不能包括重复的键,值可以重复;每个键只能对应一个值。
- Map中常用的容器为HashMap,TreeMap等。
4.1.2 Map 的常用方法
4.2 HashMap容器类
HashMap是Map接口的接口实现类,它采用哈希算法实现,是Map接口最常用的实现类。由于底层采用了哈希表存储数据,所以要求键不能重复,如果发生重复新的值会替换旧的值。HashMap在查找、删除、修改方面都有非常高的效率。
4.2.1 添加元素
public class HashMapTest {
public static void main(String[] args) {
//实例化 HashMap 容器
Map<String,String> map = new HashMap<>();
//添加元素
map.put("a","A");
String value = map.put("a","B");
System.out.println(value);
}
}
4.2.2 获取元素
4.2.2.1 方式一
通过 get 方法获取元素
String val = map.get("a");
System.out.println(val);
4.2.2.2 方式二
通过 keySet 方法获取元素
//获取 HashMap 容器中所有的元素,可以使用 keySet 方法与 get 方法一并完成。
Set<String> keys = map.keySet();
for(String key:keys){
String v1 = map.get(key);
System.out.println(key+" ---- "+v1);
}
4.2.2.3 方式三
通过 entrySet 方法获取 Map.Entry 类型获取元素
Set<Map.Entry<String,String>> entrySet = map.entrySet();
for(Map.Entry<String,String> entry:entrySet){
String key = entry.getKey();
String v = entry.getValue();
System.out.println(key+" ---------- "+v);
}
4.2.3 Map 容器的并集操作
Map<String,String> map2 = new HashMap<>();
map2.put("f","F");
map2.put("c","cc");
map2.putAll(map);
Set<String> keys2 = map2.keySet();
for(String key:keys2){
System.out.println("key: "+key+" Value: "+map2.get(key));
}
4.2.4 删除元素
String v = map.remove("e");
System.out.println(v);
Set<String> keys3 = map.keySet();
for(String key:keys3){
System.out.println("key: "+key+" Value: "+map.get(key));
}
4.2.5 判断 key 或 value 是否存在
4.2.5.1 判断 key 是否存在
boolean flag = map.containsKey("a");
System.out.println(flag);
4.2.5.2 判断 value 是否存在
boolean flag2 = map.containsValue("B");
System.out.println(flag2);
4.2.6 HashMap 的底层源码分析
4.2.6.1 底层存储介绍
HashMap底层实现采用了哈希表,这是一种非常重要的数据结构。对于我们以后理解很多技术都非常有帮助,因此,非常有必要让大家详细的理解。
数据结构中由数组和链表来实现对数据的存储,他们各有特点。
(1)数组:占用空间连续。寻址容易,查询速度快。但是,增加和删除效率非常低。
(2)链表:占用空间不连续。寻址困难,查询速度慢。但是,增加和删除效率非常高。那么,我们能不能结合数组和链表的优点(即查询快,增删效率也高)呢?答案就是“哈希表”。哈希表的本质就是“数组+链表”。
老鸟 建议
对于频繁出现的“底层实现”讲解,建议学有余力的童鞋将它搞通。刚入门的童鞋如果觉得有难度,可以暂时跳过。入门期间,掌握如何使用即可,底层原理是扎实内功,便于大家应对一些大型企业的笔试面试。
4.2.6.2 成员变量
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* The number of key-value mappings contained in this map.
*/
transient int size;
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] tabl
4.2.6.3 HashMap 中存储元素的节点类型
4.2.6.3.1 Node 类
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
4.2.6.3.2 TreeNode 类
/**
* Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
* extends Node) so can be used as extension of either regular or
* linked node.
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
/**
* Returns root of tree containing this node.
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
4.2.6.3.3 它们的继承关系
4.2.6.4 数组初始化
在 JDK1.8 的 HashMap 中对于数组的初始化采用的是延迟初始化方式。通过 resize 方法实现初始化处理。resize 方法既实现数组初始化,也实现数组扩容处理。
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR *
DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft <
(float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
4.2.6.5 计算 Hash 值
(1) 获得 key 对象的 hashcode
首先调用 key 对象的 hashcode()方法,获得 key 的 hashcode 值。
(2) 根据 hashcode 计算出 hash 值(要求在[0, 数组长度-1]区间)hashcode 是一个整数,我们需要将它转化成[0, 数组长度-1]的范围。我们要求转化后的 hash 值尽量均匀地分布在[0,数组长度-1]这个区间,减少“hash 冲突”
i. 一种极端简单和低下的算法是:
hash 值 = hashcode/hashcode;
也就是说,hash 值总是 1。意味着,键值对对象都会存储到数组索引 1 位置,这样就形成一个非常长的链表。相当于每存储一个对象都会发生“hash冲突”,HashMap 也退化成了一个“链表”。
ii. 一种简单和常用的算法是(相除取余算法):
hash 值 = hashcode%数组长度
这种算法可以让 hash 值均匀的分布在[0,数组长度-1]的区间。但是,这种算法由于使用了“除法”,效率低下。JDK 后来改进了算法。首先约定数组长度必须为 2 的整数幂,这样采用位运算即可实现取余的效果:hash 值 = hashcode&(数组长度-1)。
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key,value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
4.2.6.6 添加元素
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key,value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
4.2.6.7 数组扩容
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key,
value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
t reeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR *DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
4.3TreeMap 容器类
TreeMap 和 HashMap 同样实现了 Map 接口,所以,对于 API 的用法来说是没有区别的。HashMap 效率高于 TreeMap;TreeMap 是可以对键进行排序的一种容器,在需要对键排序时可选用 TreeMap。TreeMap 底层是基于红黑树实现的。
在使用 TreeMap 时需要给定排序规则:
- 元素自身实现比较规则
- 通过比较器实现比较规
4.3.1 元素自身实现比较规则
public class Users implements Comparable<Users>{
private String username;
private int userage;
public Users(String username, int userage) {
this.username = username;
this.userage = userage;
}
public Users() {
}
@Override
public boolean equals(Object o) {
System.out.println("equals...");
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Users users = (Users) o;
if (userage != users.userage) return false;
return username != null ? username.equals(users.username) : users.username == null;
}
@Override
public int hashCode() {
int result = username != null ? username.hashCode() : 0;
result = 31 * result + userage;
return result;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getUserage() {
return userage;
}
public void setUserage(int userage) {
this.userage = userage;
}
@Override
public String toString() {
return "Users{" +
"username='" +
", userage=" + userage +
'}';
}
//定义比较规则
//正数:大,负数:小,0:相等
@Override
public int compareTo(Users o) {
if(this.userage < o.getUserage()){
return 1;
}
if(this.userage == o.getUserage()){
return this.username.compareTo(o.getUsername());
}
return -1;
}
}
public class TreeMapTest {
public static void main(String[] args) {
//实例化 TreeMap
Map<Users,String> map = new TreeMap<>();
Users u1 = new Users("oldlu",18);
Users u2 = new Users("admin",22);
Users u3 = new Users("sxt",22);
map.put(u1,"oldlu");
map.put(u2,"admin");
map.put(u3,"sxt");
Set<Users> keys = map.keySet();
for(Users key :keys){
System.out.println(key+" --------- "+map.get(key));
}
}
}
4.3.2 通过比较器实现比较规则
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public Student() {
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
if (age != student.age) return false;
return name != null ? name.equals(student.name) : student.name == null;
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
}
public class StudentComparator implements Comparator<Student> {
//定义比较规则
@Override
public int compare(Student o1, Student o2) {
if(o1.getAge() > o2.getAge()){
return 1;
}
if(o1.getAge() == o2.getAge()){
return o1.getName().compareTo(o2.getName());
}
return -1;
}
}
Map<Student,String> treeMap = new TreeMap<>(new
StudentComparator());
Student s1 = new Student("oldlu",18);
Student s2 = new Student("admin",22);
Student s3 = new Student("sxt",22);
treeMap.put(s1,"oldlu");
treeMap.put(s2,"admin");
treeMap.put(s3,"sxt");
Set<Student> keys1 = treeMap.keySet();
for(Student key :keys1){
System.out.println(key+" ---- "+treeMap.get(key));
}
5 Iterator 迭代器
5.1Iterator 迭代器接口介绍
Collection接口继承了Iterable接口,在该接口中包含一个名为iterator的抽象方法,所有实现了Collection接口的容器类对该方法做了具体实现。iterator方法会返回一个Iterator接口类型的迭代器对象,在该对象中包含了三个方法用于实现对单例容器的迭代处理。
Iterator对象的工作原理:
- boolean hasNext(); //判断游标当前位置是否有元素,如果有返回true,否则返回false;
- Object next(); //获取当前游标所在位置的元素,并将游标移动到下一个位置;
- void remove(); //删除游标当前位置的元素,在执行完next后该操作只能执行一次;
5.2迭代器的使用
5.2.1 使用 Iterator 迭代 List 接口类型容器
public class IteratorListTest {
public static void main(String[] args) {
//实例化容器
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
//获取元素
//获取迭代器对象
Iterator<String> iterator = list.iterator();
//方式一:在迭代器中,通过 while 循环获取元素
while(iterator.hasNext()){
String value = iterator.next();
System.out.println(value);
}
System.out.println("-------------------------------");
//方法二:在迭代器中,通过 for 循环获取元素
for(Iterator<String> it = list.iterator();it.hasNext();){
String value = it.next();
System.out.println(value);
}
}
}
5.2.2 使用 Iterator 迭代 Set 接口类型容器
public class IteratorSetTest {
public static void main(String[] args) {
//实例化 Set 类型的容器
Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("c");
//方式一:通过 while 循环
//获取迭代器对象
Iterator<String> iterator = set.iterator();
while(iterator.hasNext()){
String value = iterator.next();
System.out.println(value);
}
System.out.println("-------------------------");
//方式二:通过 for 循环
for(Iterator<String> it = set.iterator();it.hasNext();){
String value = it.next();
System.out.println(value);
}
}
}
5.2.3 在迭代器中删除元素
public class IteratorRemoveTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
//不要在一次循环中多次调用 next 方法。
String value = iterator.next();
if("c".equals(value)){
iterator.remove();
}
}
System.out.println("----------------");
for(Iterator<String> it = list.iterator();it.hasNext();){
System.out.println(it.next());
list.add("dddd");
}
}
}
6 Collections 工具类
Collections 是一个工具类,它提供了对 Set、List、Map 进行排序、填充、查找元素的辅助方法。该类中所有的方法都为静态方法。
常用方法:
- void sort(List) //对 List 容器内的元素排序,排序的规则是按照升序进行排序。
- void shuffle(List) //对 List 容器内的元素进行随机排列。
- void reverse(List) //对 List 容器内的元素进行逆续排列 。
- void fill(List, Object) //用一个特定的对象重写整个 List 容器。
- int binarySearch(List, Object)//对于顺序的 List 容器,采用折半查找的方法查找特定对象。
6.1对 List 类型容器进行处理
public class CollectionsSortTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("c");
list.add("b");
list.add("d");
list.add("a");
//通过 Collections 工具类中的 sort 方法完成排序
Collections.sort(list);
for(String str:list){
System.out.println(str);
}
}
}
6.2对 List 类型容器进行随机排序
List<String> list2 = new ArrayList<>();
list2.add("a");
list2.add("b");
list2.add("c");
list2.add("d");
//洗牌处理
Collections.shuffle(list2);
for(String str:list2){
System.out.println(str);
}