Java基础3常用类、异常、容器

第八章 常用类

包装类

8.1.1包装类基本知识

Java是面向对象的语言,但并不是“纯面向对象”的,因为我们经常用到的基本数据类型就不是对象。但是我们在实际应用中经常需要将基本数据转化成对象,以便于操作。
比如:将基本数据类型存储到Object[]数组或集合中的操作等等。

为了解决这个不足,Java在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八个和基本数据类型对应的类统称为包装类(Wrapper Class)。

包装类均位于java.lang包,八种包装类和基本数据类型的对应关系如表8-1所示:
在这里插入图片描述
在这八个类名中,除了Integer和Character类以外,其它六个类的类名和基本数据类型一致,只是类名的第一个字母大写而已。

在这八个类中,除了Character和Boolean以外,其他的都是“数字型”,“数字型”都是java.lang.Number的子类。Number类是抽象类,因此它的抽象方法,所有子类都需要提供实现。Number类提供了抽象方法:intValue()、longValue()、floatValue()、doubleValue(),意味着所有的“数字型”包装类都可以互相转型。如图8-1和图8-2所示。
在这里插入图片描述
下面我们通过一个简单的示例认识一下包装类。

【示例8-1】初识包装类

public class WrapperClassTest {
    public static void main(String[] args) {
        Integer i = new Integer(10);
        Integer j = new Integer(50);
    }
}

示例8-1的内存分析如图8-3所示:
在这里插入图片描述

8.1.2包装类的用途

对于包装类来说,这些类的用途主要包含两种:
1.作为和基本数据类型对应的类型存在,方便涉及到对象的操作,如Object[]、集合等的操作。
2.包含每种基本数据类型的相关属性如最大值、最小值等,以及相关的操作方法(这些操作方法的作用是在基本数据类型、包装类对象、字符串之间提供相互之间的转化!)。
在这里插入图片描述
方法
valueOf 基本数据类型转成包装类对象
intValue 把包装类对象转成基本数据类型
parseInt 字符串转化成包装类Integer对象
toString 包装类 Integer对象转化成字符串
MAX_VALUE
【示例8-2】包装类的使用

public class Test {
    /** 测试Integer的用法,其他包装类与Integer类似 */
    void testInteger() {
        //基本数据类型转成包装类对象
        // 基本类型转化成Integer对象,有两种方法
        Integer int1 = new Integer(10);
        Integer int2 = Integer.valueOf(20); // 官方推荐这种写法
       
        //把包装类对象转成基本数据类型
        // Integer对象转化成int
        int a = int1.intValue();
        double d=b.doubleValue();
        
        // 字符串转化成包装类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();
    }
}

执行结果如图8-4所示:
在这里插入图片描述
8.1.3自动装箱和拆箱
自动装箱和拆箱就是将基本数据类型和包装类之间进行自动的互相转换。JDK1.5后,Java引入了自动装箱(autoboxing)/拆箱(unboxing)。

自动装箱:
基本类型的数据处于需要对象的环境中时,会自动转为“对象”
我们以Integer为例:在JDK1.5以前,这样的代码 Integer i = 5 是错误的,必须要通过Integer i = new Integer(5) 这样的语句来实现基本数据类型转换成包装类的过程;而在JDK1.5以后,Java提供了自动装箱的功能,因此只需Integer i = 5这样的语句就能实现基本数据类型转换成包装类,这是因为JVM为我们执行了**Integer i = Integer.valueOf(5)**这样的操作,这就是Java的自动装箱。

自动拆箱:
每当需要一个值时,对象会自动转成基本数据类型,没必要再去显式调用**intValue()、doubleValue()**等转型方法。
Integer i = 5;int j = i; 这样的过程就是自动拆箱。

我们可以用一句话总结自动装箱/拆箱:
自动装箱过程是通过调用包装类的valueOf()方法实现的,而自动拆箱过程是通过调用包装类的 xxxValue()方法实现的(xxx代表对应的基本数据类型,如intValue()、doubleValue()等)。
自动装箱与拆箱的功能事实上是编译器来帮的忙,编译器在编译时依据您所编写的语法,决定是否进行装箱或拆箱动作,如示例8-3与示例8-4所示。
在这里插入图片描述
在这里插入图片描述
空指针错误是对象为空,但是你调用了它的方法。
因为c是对象,你自动调用了c的intValue()方法
所以我们这里判断一下

【示例8-3】自动装箱

Integer i = 100;//自动装箱
//相当于编译器自动为您作以下的语法编译:
Integer i = Integer.valueOf(100);//调用的是valueOf(100),而不是new Integer(100)
【示例8-4】自动拆箱

Integer i = 100;
int j = i;//自动拆箱
//相当于编译器自动为您作以下的语法编译:
int j = i.intValue();

所以自动装箱与拆箱的功能是所谓的“编译器蜜糖(Compiler Sugar)”,虽然使用这个功能很方便,但在程序运行阶段您得了解Java的语义。例如示例8-5所示的程序是可以通过编译的:

【示例8-5】包装类空指针异常问题

public class Test1 {
    public static void main(String[] args) {
        Integer i = null;
        int j = i;
    }
}

执行结果如图8-5所示:
在这里插入图片描述
示例8-5的运行结果之所以会出现空指针异常,是因为示例8-5中的代码相当于:

public class Test1 {
public static void main(String[] args) {
//示例8-5的代码在编译时期是合法的,但是在运行时期会有错误,因为其相当于:
Integer i = null;
int j = i.intValue();
}
}
null表示i没有指向任何对象的实体,但作为对象名称是合法的(不管这个对象名称存是否指向了某个对象的实体)。由于实际上i并没有指向任何对象的实体,所以也就不可能操作intValue()方法,这样上面的写法在运行时就会出现NullPointerException错误。

【示例8-6】自动装箱与拆箱

public class Test2 {
    /**
     * 测试自动装箱和拆箱 结论:虽然很方便,但是如果不熟悉特殊情况,可能会出错!
     */
    public static void main(String[] args) {
        Integer b = 23; // 自动装箱
        int a = new Integer(20); //自动拆箱
        // 下面的问题我们需要注意:
        Integer c = null;
        int d = c; // 此处其实就是:c.intValue(),因此抛空指针异常。
    }
}

8.1.4包装类的缓存问题

整型、char类型所对应的包装类,在自动装箱时,对于-128~127之间的值会进行缓存处理,其目的是提高效率。

缓存处理的原理为:如果数据在-128~127这个区间,那么在类加载时就已经为该区间的每个数值创建了对象,并将这256个对象存放到一个名为cache的数组中。每当自动装箱过程发生时(或者手动调用valueOf()时),就会先判断数据是否在该区间,如果在则直接获取数组中对应的包装类对象的引用,如果不在该区间,则会通过new调用包装类的构造方法来创建对象。

下面我们以Integer类为例,看一看Java为我们提供的源码,加深对缓存技术的理解,如示例8-7所示。

【示例8-7】Integer类相关源码如下:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

这段代码中我们需要解释下面几个问题:
1.IntegerCache类为Integer类的一个静态内部类,仅供Integer类使用。
2.一般情况下 IntegerCache.low为-128,IntegerCache.high为127,IntegerCache.cache为内部类的一个静态属性,如示例8-8所示。

【示例8-8】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所示。
【示例8-9】测试代码

public class Test3 {
    public static void main(String[] args) {
        //缓存[-128,127]之间的数字。实际就是系统初始的时候,创建了[-128,127]之间的一个缓存数组。
        //当我们调用valueOf()的时候。首先检查是否在[-128,127]之间。如果在这个范围则直接从缓存数组中拿出已经创
        //创建好的对象。如果不在这个范围内,则创建新的Integer对象。
        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,equals()方法是比较数值
    }
}

执行结果如图8-6所示:
在这里插入图片描述
示例8-9的内存分析如图8-7所示:
在这里插入图片描述
注意
1.JDK1.5以后,增加了自动装箱与拆箱功能,如:Integer i = 100; int j = new Integer(100);
2.自动装箱调用的是valueOf()方法,而不是new Integer()方法。
3.自动拆箱调用的xxxValue()方法。
4.包装类在自动装箱时为了提高效率,对于-128~127之间的值会进行缓存处理。超过范围后,对象之间不能再使用==进行数值的比较,而是使用equals方法

String类

8.2.1 String类

String 类对象代表不可变的Unicode字符序列,因此我们可以将String对象称为“不可变对象”。 那什么叫做“不可变对象”呢?指的是对象内部的成员变量的值无法再改变。我们打开String类的源码,如图8-8所示:
在这里插入图片描述
我们发现字符串内容全部存储到value[]数组中,而变量value是final类型的,也就是常量(即只能被赋值一次)。 这就是“不可变对象”的典型定义方式。

String类型是不能再变的,所以我们要改变字符串时就需要新创建一个字符串变量。

我们发现在前面学习String的某些方法,比如:substring()是对字符串的截取操作,但本质是读取原字符串内容生成了新的字符串。测试代码如下:
【示例8-10】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()));
    }
}

执行结果如图8-9所示:
在这里插入图片描述
在遇到字符串常量之间的拼接时,编译器会做出优化,即在编译期间就会完成字符串的拼接。因此,在使用==进行String对象之间的比较时,我们需要特别注意,如示例8-11所示。

【示例8-11】字符串常量拼接时的优化

public class TestString2 {
    public static void main(String[] args) {
        //编译器做了优化,直接在编译的时候将字符串进行拼接
        String str1 = "hello" + " java";//相当于str1 = "hello java";
        String str2 = "hello java";
        System.out.println(str1 == str2);//true 
        
        String str3 = "hello";
        String str4 = " java";
        //编译的时候不知道变量中存储的是什么,所以没办法在编译的时候优化
        String str5 = str3 + str4;
        System.out.println(str2 == str5);//false
    }
}

执行结果如图8-10所示:
在这里插入图片描述

String类常用的方法有(可翻到第五章5.11.4查看,已讲过,此处不赘述):
1.String类的下述方法能创建并返回一个新的String对象: concat()、 replace()、substring()、 toLowerCase()、 toUpperCase()、trim()。
2.提供查找功能的有关方法: endsWith()、 startsWith()、 indexOf()、lastIndexOf()。
3.提供比较功能的方法: equals()、equalsIgnoreCase()、compareTo()。
4.其它方法: charAt() 、length()。

8.2.2 StringBuffer和StringBuilder

StringBuffer和StringBuilder非常类似,均代表可变的字符序列
这两个类都是抽象类AbstractStringBuilder的子类,方法几乎一模一样。

我们打开AbstractStringBuilder的源码,如示例8-11所示:
【示例8-11】AbstractStringBuilder 部分源码

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char value[];
//以下代码省略
}

显然,内部也是一个字符数组,但这个字符数组没有用final修饰,随时可以修改。因此,StringBuilder和StringBuffer称之为“可变字符序列”。那两者有什么区别呢?
1.StringBuffer JDK1.0版本提供的类,线程安全,做线程同步检查, 效率较低
2.StringBuilder JDK1.5版本提供的类,线程不安全,不做线程同步检查,因此效率较高。 建议采用该类。

例:可变字符序列
在这里插入图片描述

· 常用方法列表:
1.重载的public StringBuilder append(…)方法
可以为该StringBuilder 对象添加字符序列,仍然返回自身对象。
2.方法 public StringBuilder delete(int start,int end)
可以删除从start开始到end-1为止的一段字符序列,仍然返回自身对象。
3.方法 public StringBuilder deleteCharAt(int index)
移除此序列指定位置上的 char,仍然返回自身对象。
4.重载的public StringBuilder insert(…)方法
可以为该StringBuilder 对象在指定位置插入字符序列,仍然返回自身对象。
5.方法 public StringBuilder reverse()
用于将字符序列逆序,仍然返回自身对象。
6.方法 public String toString() 返回此序列中数据的字符串表示形式。
7.和 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)

【示例8-12】StringBuffer/StringBuilder基本用法

public class TestStringBufferAndBuilder 1{
    public static void main(String[] args) {
        /**StringBuilder*/
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 7; i++) {
            sb.append((char) ('a' + i));//追加单个字符  (char) ('a' + i)代表了先是Ascii数字转化为字符
        }
        System.out.println(sb.toString());//转换成String输出
        
        sb.append(", I can sing my abc!");//追加字符串
        System.out.println(sb.toString());
        
        /**StringBuffer*/
        StringBuffer sb2 = new StringBuffer("中华人民共和国");
        sb2.insert(0, "爱").insert(0, "我");//插入字符串   链式调用。核心就是:该方法调用了return this,把自己又返回来
        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());//字符串逆序
    }
}

执行结果如图8-11所示:
在这里插入图片描述

8.2.3 不可变和可变字符序列使用陷阱

· String使用的陷阱
String一经初始化后,就不会再改变其内容了。对String字符串的操作实际上是对其副本(原始拷贝)的操作,原来的字符串一点都没有改变。比如:
String s =“a”; 创建了一个字符串
s = s+“b”;
实际上原来的"a"字符串对象已经丢弃了,现在又产生了另一个字符串s+“b”(也就是"ab")。 如果多次执行这些改变串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。如果这样的操作放到循环中,会极大影响程序的时间和空间性能,甚至会造成服务器的崩溃。

相反,StringBuilder和StringBuffer类是对原字符串本身操作的,可以对字符串进行修改而不产生副本拷贝或者产生少量的副本。因此可以在循环中使用。

【示例8-13】String和StringBuilder在频繁字符串修改时效率测试

public class Test {
    public static void main(String[] args) {
        /**使用String进行字符串的拼接*/
        String str8 = "";
        //本质上使用StringBuilder拼接, 但是每次循环都会生成一个StringBuilder对象
        long num1 = Runtime.getRuntime().freeMemory();//获取系统剩余内存空间
        long time1 = System.currentTimeMillis();//获取系统的当前时间
        for (int i = 0; i < 5000; i++) {
            str8 = str8 + i;//相当于产生了10000个对象
        }
        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));
    }
}

执行结果如图8-12所示: 在这里插入图片描述
以后遇到这种循环累加字符串时,要用StringBuilder。
要点:
1.String:不可变字符序列。
2.StringBuffer:可变字符序列,并且线程安全,但是效率低。
3.StringBuilder:可变字符序列,线程不安全,但是效率高(一般用它)。

8.3 时间处理相关类

“时间如流水,一去不复返”,时间是一个一维的东东。所以,我们需要一把刻度尺来表达和度量时间。在计算机世界,我们把1970 年 1 月 1 日 00:00:00定为基准时间,每个度量单位是毫秒(1秒的千分之一),如图8-13所示。
在这里插入图片描述
我们用long类型的变量来表示时间,从基准时间往前几亿年,往后几亿年都能表示。如果想获得现在时刻的“时刻数值”,可以使用:

long now = System.currentTimeMillis();
这个“时刻数值”是所有时间类的核心值,年月日都是根据这个“数值”计算出来的。我们工作学习涉及的时间相关类有如下这些: 在这里插入图片描述

8.3.1 Date时间类(java.util.Date)

在标准Java类库中包含一个Date类。它的对象表示一个特定的瞬间,精确到毫秒。
1.Date() 分配一个Date对象,并初始化此对象为系统当前的日期和时间,可以精确到毫秒)。
2.Date(long date) 分配 Date 对象并初始化此对象,以表示自从标准基准时间(称为“历元(epoch)”,即 1970 年 1 月 1 日 00:00:00 GMT)以来的指定毫秒数。
3.boolean after(Date when) 测试此日期是否在指定日期之后。
4.booleanbefore(Date when) 测试此日期是否在指定日期之前。
5.boolean equals(Object obj) 比较两个日期的相等性。
6.long getTime() 返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数。
7.String toString() 把此 Date 对象转换为以下形式的 String:
dow mon dd hh:mm:ss zzz yyyy 其中: dow 是一周中的某一天 (Sun、 Mon、Tue、Wed、 Thu、 Fri、 Sat)。
在这里插入图片描述
Date源码
在这里插入图片描述
【示例8-14】Date类的使用

import java.util.Date;
public class TestDate {
    public static void main(String[] args) {
        Date date1 = new Date();
        System.out.println(date1.toString());
        long i = date1.getTime();
        Date date2 = new Date(i - 1000);
        Date date3 = new Date(i + 1000);
        System.out.println(date1.after(date2));//true
        System.out.println(date1.before(date2));//false
        System.out.println(date1.equals(date2));//false
        System.out.println(date1.after(date3));//false
        System.out.println(date1.before(date3));//true
        System.out.println(date1.equals(date3));//false
        System.out.println(new Date(1000L * 60 * 60 * 24 * 365 * 39L).toString());
    }
}

执行结果如图8-15所示:
在这里插入图片描述
查看API文档大家可以看到其实Date类中的很多方法都已经过时了。JDK1.1之前的Date包含了:日期操作、字符串转化成时间对象等操作。JDK1.1之后,日期操作一般使用Calendar类,而字符串的转化使用DateFormat类。

8.3.2 DateFormat类和SimpleDateFormat类

·DateFormat类的作用
时间对象转化成指定格式的字符串。反之,把指定格式的字符串转化成时间对象
DateFormat是一个抽象类,一般使用它的的子类SimpleDateFormat类来实现。
在这里插入图片描述
在这里插入图片描述
把时间转成字符串
在这里插入图片描述
把字符串转化为时间

在这里插入图片描述
【示例8-15】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);//2017-05-17 05:43:00
		System.out.println(s2.format(new Date()));//2017-05-17
		System.out.println(new SimpleDateFormat("hh:mm:ss").format(new Date()));//05:43:00
		
		// 将符合指定格式的字符串转成成时间对象.字符串格式需要和指定格式一致。
		String time = "2007-10-7";
		Date date = s2.parse(time);
		System.out.println("date1: " + date);//date1:Sun Oct 07 00:00:00 CST 2007
		time = "2007-10-7 20:15:30";
		date = s1.parse(time);
		System.out.println("date2: " + date);
	}
}

执行结果如图8-16所示:
在这里插入图片描述
在这里插入图片描述

时间格式字符也可以为我们提供其他的便利。比如:获得当前时间是今年的第几天。代码如下:
【示例8-16】时间格式字符的使用

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);
	}
}

执行结果如图8-17所示:
在这里插入图片描述

8.3.3 Calendar日历类

Calendar 类是一个抽象类,为我们提供了关于日期计算的相关功能,比如:年、月、日、时、分、秒的展示和计算。
GregorianCalendar 是 Calendar 的一个具体子类,提供了世界上大多数国家/地区使用的标准日历系统。

菜鸟雷区:
注意月份的表示,一月是0,二月是1,以此类推,12月是11。 因为大多数人习惯于使用单词而不是使用数字来表示月份,这样程序也许更易读,父类Calendar使用常量来表示月份:JANUARY、FEBRUARY等等。
【示例8-17】GregorianCalendar类和Calendar类的使用

import java.util.*;
public class TestCalendar {
    public static void main(String[] args) {
        // 得到相关日期元素get
        GregorianCalendar calendar = new GregorianCalendar(2999, 10, 9, 22, 10, 50);
        int year = calendar.get(Calendar.YEAR); // 打印:1999 年份
        int month = calendar.get(Calendar.MONTH); // 打印:10    月份
        int day = calendar.get(Calendar.DAY_OF_MONTH); // 打印:9  几号
        int day2 = calendar.get(Calendar.DATE); // 打印:9  几号
        // 日:Calendar.DATE和Calendar.DAY_OF_MONTH同义    
        int date = calendar.get(Calendar.DAY_OF_WEEK); // 打印:3  星期几
        // 星期几 这里是: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, 2999);
        calendar2.set(Calendar.MONTH, Calendar.FEBRUARY); // 或者用数字:月份数0-11
        calendar2.set(Calendar.DATE, 3);
        calendar2.set(Calendar.HOUR_OF_DAY, 10);
        calendar2.set(Calendar.MINUTE, 20);
        calendar2.set(Calendar.SECOND, 23);
        printCalendar(calendar2);

        // 日期计算set
        GregorianCalendar calendar3 = new GregorianCalendar(2999, 10, 9, 22, 10, 50);
        calendar3.add(Calendar.MONTH, -7); // 月份减7
        calendar3.add(Calendar.DATE, 7); // 增加7天
        printCalendar(calendar3);
        
        // 日历对象和时间对象转化
        Date d = calendar3.getTime();   //日历类转化为时间对象getTime
        GregorianCalendar calendar4 = new GregorianCalendar();
        calendar4.setTime(new Date());//时间对象转化为日期类setTime
        long g = System.currentTimeMillis();
    }


    static void printCalendar(Calendar calendar) {
        int year = calendar.get(Calendar.YEAR);
        int month = calendar.get(Calendar.MONTH) + 1;//0-11所以要加1
        int day = calendar.get(Calendar.DAY_OF_MONTH);
        int date = calendar.get(Calendar.DAY_OF_WEEK) - 1; // 星期几,  1是周日,2是周一,3是周二,1-7要分别减一
        String week = "" + ((date == 0) ? "日" : date);//0改为周日
        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);
    }
}

执行结果如图8-18所示:
在这里插入图片描述

编写程序,利用GregorianCalendar类,打印当前月份的日历,今天的日期是 2017-05-18 ,如图8-19所示为今日所在月份的日历:
在这里插入图片描述
在这里插入图片描述
【示例8-18】可视化日历的编写

import java.text.ParseException;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Scanner;
public class TestCalendar2 {
    public static void main(String[] args) throws ParseException {
        System.out.println("请输入日期(格式为:2010-3-3):");
        Scanner scanner = new Scanner(System.in);
        String dateString = scanner.nextLine(); // 2010-3-1  拿到用户输入的字符串
        // 将输入的字符串转化成日期类
        System.out.println("您刚刚输入的日期是:" + dateString);
        String[] str = dateString.split("-");
        int year = Integer.parseInt(str[0]);
        int month = new Integer(str[1]);
        int day = new Integer(str[2]);
        Calendar c = new GregorianCalendar(year, month - 1, day); // Month:0-11
      
        // 大家自己补充另一种方式:将字符串通过SImpleDateFormat转化成Date对象,
        //再将Date对象转化成日期类
        // SimpleDateFormat sdfDateFormat = new SimpleDateFormat("yyyy-MM-dd");
        // Date date = sdfDateFormat.parse(dateString);
        // Calendar c = new GregorianCalendar();
        // c.setTime(date);
        // int day = c.get(Calendar.DATE);
        
        c.set(Calendar.DATE, 1);//当前日期置为1
        int dow = c.get(Calendar.DAY_OF_WEEK); // week:1-7 日一二三四五六不见得每月1号都是周日
        
        System.out.println("日\t一\t二\t三\t四\t五\t六");
        for (int i = 0; i < dow - 1; i++) {
            System.out.print("\t");
        }
        
        int maxDate = c.getActualMaximum(Calendar.DATE);//不见得每个月都是31天
        for (int i = 1; i <= maxDate; i++) {
        
            StringBuilder sBuilder = new StringBuilder();
            if (c.get(Calendar.DATE) == day) {                        //当前日加*
                sBuilder.append(c.get(Calendar.DATE) + "*\t");
            } else {
                sBuilder.append(c.get(Calendar.DATE) + "\t");
            }
            System.out.print(sBuilder);

            if (c.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) {//每逢周六换行
                System.out.print("\n");
            }
            c.add(Calendar.DATE, 1);//循环后日加1
        }
    }
}

8.4 Math类

java.lang.Math提供了一系列静态方法用于科学计算;其方法的参数和返回值类型一般为double型。如果需要更加强大的数学运算能力,计算高等数学中的相关内容,可以使用apache commons下面的Math类库。

Math类的常用方法:
1.abs 绝对值
2. acos,asin,atan,cos,sin,tan 三角函数
3.sqrt 平方根
4. pow(double a, double b) a的b次幂
5. max(double a, double b) 取大值
6. min(double a, double b) 取小值
7. ceil(double a) 大于a的最小整数
8. floor(double a) 小于a的最大整数
9. random() 返回 0.0 到 1.0 的随机数
10. long round(double a) double型的数据a转换为long型(四舍五入)
11. toDegrees(double angrad) 弧度->角度
12. toRadians(double angdeg) 角度->弧度
【示例8-19】Math类的常用方法

public class TestMath {
    public static void main(String[] args) {
        //取整相关操作
        System.out.println(Math.ceil(3.2));//4.0
        System.out.println(Math.floor(3.2));//3.0
        System.out.println(Math.round(3.2));//3
        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)
    }
}

执行结果如图8-20所示:
在这里插入图片描述

Math类中虽然为我们提供了产生随机数的方法Math.random(),但是通常我们需要的随机数范围并不是[0, 1)之间的double类型的数据,这就需要对其进行一些复杂的运算。如果使用Math.random()计算过于复杂的话,我们可以使用例外一种方式得到随机数,即Random类,这个类是专门用来生成随机数的,并且Math.random()底层调用的就是Random的nextDouble()方法。
【示例8-20】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));
        //随机生成[20,30)之间的int类型的数据(此种方法计算较为复杂)
        System.out.print(20 + (int) (rand.nextDouble() * 10));
    }
}

执行结果如图8-21所示:
在这里插入图片描述
注意:Random类位于java.util包下。

8.5.1 File类的基本用法

java.io.File类:代表文件目录。 在开发中,读取文件、生成文件、删除文件、修改文件的属性时经常会用到本类。
File类的常见构造方法:public File(String pathname)
以pathname为路径创建File对象,如果pathname是相对路径,则默认的当前路径在系统属性user.dir中存储,如示例8-21所示。

【示例8-21】文件的创建

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();
    }
}

在eclipse项目开发中,user.dir就是本项目的目录。因此,执行完毕后,在本项目和D盘下都生成了新的文件(如果是eclipse下,一定按F5刷新目录结构才能看到新文件)。如图8-22所示。
在这里插入图片描述
在这里插入图片描述
【示例8-22】测试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.getPath());
    }
}

执行结果如图8-23所示: 在这里插入图片描述

通过File对象创建空文件目录(在该对象所指的文件或目录不存在的情况下)
表8-4 File类创建文件或目录的方法列表
在这里插入图片描述
【示例8-23】使用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);//创建失败
    }
}

执行结果如图8-24所示:
在这里插入图片描述
【示例8-24】使用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);//创建成功
    }
}

执行结果如图8-25所示:
在这里插入图片描述
【示例8-25】File类的综合应用

import java.io.File;
import java.io.IOException;
public class TestFile5 {
    public static void main(String[] args) {
        //指定一个文件
        File file = new File("d:/sxt/b.txt");
        //判断该文件是否存在
        boolean flag= file.exists();
        //如果存在就删除,如果不存在就创建
        if(flag){
            //删除
            boolean flagd = file.delete();
            if(flagd){
                System.out.println("删除成功");
            }else{
                System.out.println("删除失败");
            }
        }else{
            //创建
            boolean flagn = true;
            try {
                //如果目录不存在,先创建目录
                File dir = file.getParentFile();
                dir.mkdirs();
                //创建文件
                flagn = file.createNewFile();
                System.out.println("创建成功");
            } catch (IOException e) {
                System.out.println("创建失败");
                e.printStackTrace();
            }          
        }
        //文件重命名(同学可以自己测试一下)
        //file.renameTo(new File("d:/readme.txt"));
    }
}

第一次执行结果如图8-26所示:
在这里插入图片描述

8.5.2 递归遍历目录结构和树状展现

本节结合前面给大家讲的递归算法,展示目录结构。大家可以先建立一个目录,下面增加几个子文件夹或者文件,用于测试。

【示例8-26】使用递归算法,以树状结构展示目录树

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) {//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);
			}
		}
	}
}

执行结果如图8-28所示:
在这里插入图片描述

8.6 枚举

JDK1.5引入了枚举类型。枚举类型的定义包括枚举声明和枚举体。格式如下:

enum  枚举名 {
      枚举体(常量列表)
}

枚举体就是放置一些常量。我们可以写出我们的第一个枚举类型,如示例8-27所示:
【示例8-27】创建枚举类型

enum Season {
    SPRING, SUMMER, AUTUMN, WINDER 
}

所有的枚举类型隐性地继承自 java.lang.Enum。枚举实质上还是类!而每个被枚举的成员实质就是一个枚举类型的实例,他们默认都是public static final修饰的。可以直接通过枚举类型名使用它们。

老鸟建议
1.当你需要定义一组常量时,可以使用枚举类型。
2.尽量不要使用枚举的高级特性,事实上高级特性都可以使用普通类来实现,没有必要引入枚举,增加程序的复杂性!

【示例8-28】枚举的使用

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 WINDTER:
            System.out.println("冬天");
            break;
        }
    }
}
/**季节*/
enum Season {
    SPRING, SUMMER, AUTUMN, WINDTER
}
/**星期*/
enum Week {
    星期一, 星期二, 星期三, 星期四, 星期五, 星期六, 星期日
}

本章总结

1.每一个基本数据类型对应一个包装类。
2.包装类的用途:
作为和基本数据类型对应的引用类型存在,方便涉及到对象的操作。
包含每种基本数据类型的相关属性如最大值、最小值以及相关的操作方法。
3.JDK1.5后在Java中引入自动装箱和拆箱。
4.字符串相关类String、StringBuffer与StringBuilder
String:不可变字符序列。
StringBuffer:可变字符序列,并且线程安全,但是效率低。
StringBuilder:可变字符序列,线程不安全,但是效率高(一般用它)。
日期与时间类Date、DateFormat、SimpleDateFormat、Calendar、GregorianCalendar。
5.Math类的常用方法
pow(double a,double b)
max(double a,double b)
min(double a,double b)
random()
long round(double a)
6.与操作文件相关的File类。
7.当需要定义一组常量时,使用枚举类型。

第六章 异常

6.1 导引问题

在实际工作中,我们遇到的情况不可能是非常完美的。比如:你写的某个模块,用户输入不一定符合你的要求;你的程序要打开某个文件,这个文件可能不存在或者文件格式不对;你要读取数据库的数据,数据可能是空的;我们的程序再运行着,但是内存或硬盘可能满了等等。

软件程序在运行过程中,非常可能遇到刚刚提到的这些问题,我们称之为异常,英文是:Exception,意思是例外。遇到这些例外情况,或者叫异常,我们怎么让写的程序做出合理的处理,安全的退出,而不至于程序崩溃呢?我们本章就要讲解这些问题。
如果我们要拷贝一个文件,在没有异常机制的情况下,我们需要考虑各种异常情况,伪代码如下:
【示例6-1】伪代码使用if处理程序中可能出现的各种情况

public class Test1 {
    public static void main(String[] args) {
        //将d:/a.txt复制到e:/a.txt
        if("d:/a.txt"这个文件存在){
            if(e盘的空间大于a.txt文件长度){
                if(文件复制一半IO流断掉){
                    停止copy,输出:IO流出问题!
                }else{
                    copyFile("d:/a.txt","e:/a.txt");
                }
            }else{
                System.out.println("e盘空间不够存放a.txt!");
            }
        }else{
            System.out.println("a.txt不存在!");
        }
    }
}

这种方式,有两个坏处:
1.逻辑代码和错误处理代码放一起!
2.程序员本身需要考虑的例外情况较复杂,对程序员本身要求较高!

那么,我们如何解决应对异常情况呢?Java的异常机制给我们提供了方便的处理方式。如上情况,如果是用Java的异常机制来处理,示意代码如下(仅限示意,不能运行):

try {
    copyFile("d:/a.txt","e:/a.txt");
} catch (Exception e) {
    e.printStackTrace();
}

异常机制本质:就是当程序出现错误,程序安全退出的机制。

6.2 异常(Exception)的概念

异常指程序运行过程中出现的非正常现象,例如用户输入错误、除数为零、需要处理的文件不存在、数组下标越界等。
在Java的异常处理机制中,引进了很多用来描述和处理异常的类,称为异常类。异常类定义中包含了该类异常的信息和对异常进行处理的方法。
所谓异常处理,就是指程序在出现问题时依然可以正确的执行完。
我们开始看我们的第一个异常对象,并分析一下异常机制是如何工作的。
【示例6-2】异常的分析

public class Test2 {
    public static void main(String[] args) {
        int i=1/0;  //除数为0
        System.out.println(i);
    }
}

执行结果如图6-1所示:
在这里插入图片描述
Java是采用面向对象的方式来处理异常的。处理过程:
1.抛出异常:在执行一个方法时,如果发生异常,则这个方法生成代表该异常的一个对象,停止当前执行路径,并把异常对象提交给JRE。
2. 捕获异常:JRE得到该异常后,寻找相应的代码来处理该异常。JRE在方法的调用栈中查找,从生成异常的方法开始回溯,直到找到相应的异常处理代码为止。

6.3 异常分类

JDK 中定义了很多异常类,这些类对应了各种各样可能出现的异常事件,所有异常对象都是派生于Throwable类的一个实例。如果内置的异常类不能够满足需要,还可以创建自己的异常类。

Java对异常进行了分类,不同类型的异常分别用不同的Java类表示,所有异常的根类为java.lang.Throwable,Throwable下面又派生了两个子类:Error和Exception。Java异常类的层次结构如图6-2所示。
在这里插入图片描述

6.3.1 Error

Error是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。

Error表明系统JVM已经处于不可恢复的崩溃状态中。我们不需要管它。
在这里插入图片描述
Error与Exception的区别
1.我开着车走在路上,一头猪冲在路中间,我刹车。这叫一个异常。
2. 我开着车在路上,发动机坏了,我停车,这叫错误。系统处于不可恢复的崩溃状态。发动机什么时候坏?我们普通司机能管吗?不能。发动机什么时候坏是汽车厂发动机制造商的事。

6.3.2 Exception

Exception是程序本身能够处理的异常,如:空指针异常(NullPointerException)、数组下标越界异常(ArrayIndexOutOfBoundsException)、类型转换异常(ClassCastException)、算术异常(ArithmeticException)等。

Exception类是所有异常类的父类,其子类对应了各种各样可能出现的异常事件。 通常Java的异常可分为:
1.RuntimeException 运行时异常
2.CheckedException 已检查异常

6.3.3 RuntimeException运行时异常

派生于RuntimeException的异常,如被 0 除、数组下标越界、空指针等,其产生比较频繁,处理麻烦,如果显式的声明或捕获将会对程序可读性和运行效率影响很大。 因此由系统自动检测并将它们交给缺省的异常处理程序(用户可不必对其处理)。

这类异常通常是由编程错误导致的,所以在编写程序时,并不要求必须使用异常处理机制来处理这类异常,经常需要通过增加“逻辑处理来避免这些异常”。
1
【示例6-3】ArithmeticException异常:试图除以0

public class Test3 {
    public static void main(String[] args) {
        int b=0;
        System.out.println(1/b);
    }
}

执行结果如图6-2所示:
在这里插入图片描述
解决如上异常需要修改代码:

public class Test3 {
    public static void main(String[] args) {
        int b=0;
        if(b!=0){
            System.out.println(1/b);
        }
    }
}

2
当程序访问一个空对象的成员变量或方法,或者访问一个空数组的成员时会发生空指针异常(NullPointerException)。怎么处理?
【示例6-4】NullPointerException异常

public class Test4 {
public static void main(String[] args) {
String str=null;
System.out.println(str.charAt(0));
}
}

执行结果如图6-5所示:
在这里插入图片描述
解决空指针异常,通常是增加非空判断:

public class Test4 {
    public static void main(String[] args) {
        String str=null;
        if(str!=null){
            System.out.println(str.charAt(0));
        }
    }
}

3
在引用数据类型转换时,有可能发生类型转换异常(ClassCastException)。
【示例6-5】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;
    }
}

执行结果如图6-6所示:
在这里插入图片描述
解决ClassCastException的典型方式:

public class Test5 {
    public static void main(String[] args) {
        Animal a = new Dog();
        if (a instanceof Cat) {
            Cat c = (Cat) a;
        }
    }
}

4
当程序访问一个数组的某个元素时,如果这个元素的索引超出了0~数组长度-1这个范围,则会出现数组下标越界异常(ArrayIndexOutOfBoundsException)。
【示例6-6】ArrayIndexOutOfBoundsException异常

public class Test6 {
    public static void main(String[] args) {
        int[] arr = new int[5];
        System.out.println(arr[5]);
    }
}

执行结果如图6-7所示:
在这里插入图片描述

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)。

【示例6-7】NumberFormatException异常

public class Test7 {
    public static void main(String[] args) {
        String str = "1234abcf";
        System.out.println(Integer.parseInt(str));
    }
}

执行结果如图6-8所示:
在这里插入图片描述

数字格式化异常的解决,可以引入正则表达式判断是否为数字:

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));
        }
    }
}

注意事项
1.在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。
2.运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。

6.3.4 CheckedException已检查异常

所有不是RuntimeException的异常,统称为Checked Exception,又被称为“已检查异常”,如IOException、SQLException等以及用户自定义的Exception异常。 这类异常**在编译时就必须做出处理,否则无法通过编译。**如图6-9所示。
在这里插入图片描述
如图6-9所示,异常的处理方式有两种:使用“try/catch”捕获异常、使用“throws”声明异常。

6.4 异常的处理方式之一:捕获异常

捕获异常是通过3个关键词来实现的:try-catch-finally。用try来执行一段程序,如果出现异常,系统抛出一个异常,可以通过它的类型来捕捉(catch)并处理它,最后一步是通过finally语句为异常处理提供一个统一的出口,finally所指定的代码都要被执行(catch语句可有多条;finally语句最多只能有一条,根据自己的需要可有可无)。如图6-10所示。
在这里插入图片描述
上面过程详细解析:
1.try:
try语句指定了一段代码,该段代码就是异常捕获并处理的范围。在执行过程中,当任意一条语句产生异常时,就会跳过该条语句中后面的代码。代码中可能会产生并抛出一种或几种类型的异常对象,它后面的catch语句要分别对这些异常做相应的处理。
一个try语句必须带有至少一个catch语句块或一个finally语句块 。
注意事项:当异常处理的代码执行结束以后,不会回到try语句去执行尚未执行的代码。
2.catch:
n-每个try语句块可以伴随一个或多个catch语句,用于处理可能产生的不同类型的异常对象。
n-常用方法,这些方法均继承自Throwable类 。
u-toString ()方法,显示异常的类名和产生异常的原因
u-getMessage()方法,只显示产生异常的原因,但不显示类名
u-printStackTrace()方法,用来跟踪异常事件发生时堆栈的内容
n-catch捕获异常时的捕获顺序:
u-如果异常类之间有继承关系,在顺序安排上需注意。越是顶层的类,越放在下面,再不然就直接把多余的catch省略掉。 也就是先捕获子类异常再捕获父类异常
3.finally:
n-有些语句,不管是否发生了异常,都必须要执行,那么就可以把这样的语句放到finally语句块中。
n-通常在finally中关闭程序块已打开的资源,比如:关闭文件流、释放数据库连接等。

try-catch-finally语句块的执行过程:
try-catch-finally程序块的执行流程以及执行结果比较复杂。
基本执行过程如下:
程序首先执行可能发生异常的try语句块。如果try语句没有出现异常则执行完后跳至finally语句块执行;如果try语句出现异常,则中断执行并根据发生的异常类型跳至相应的catch语句块执行处理。catch语句块可以有多个,分别捕获不同类型的异常。catch语句块执行完后程序会继续执行finally语句块。finally语句是可选的,如果有的话,则不管是否发生异常,finally语句都会被执行。

注意事项
1.即使try和catch块中存在return语句,finally语句也会执行。是在执行完finally语句后再通过return退出。
2.finally语句块只有一种情况是不会执行的,那就是在执行finally之前遇到了System.exit(0)结束程序运行。

【示例6-8】典型代码(先不要敲!!)

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();
            }
        }
    }
}

6.5 异常的处理方式之二:声明异常(throws子句)

当CheckedException产生时,不一定立刻处理它,可以再把异常throws出去。

在方法中使用try-catch-finally是由这个方法来处理异常。但是在一些情况下,当前方法并不需要处理发生的异常,而是向上传递给调用它的方法处理

如果一个方法中可能产生某种异常,但是并不能确定如何处理这种异常,则应根据异常规范在方法的首部声明该方法可能抛出的异常。

如果一个方法抛出多个已检查异常,就必须在方法的首部列出所有的异常,之间以逗号隔开。
【示例6-9】典型代码(先不敲!)

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 {
            in.close();
        }
    }
}

注意事项
1.方法重写中声明异常原则:子类重写父类方法时,如果父类方法有声明异常,那么子类声明的异常范围不能超过父类声明的范围。

6.6 自定义异常

1.在程序中,可能会遇到JDK提供的任何标准异常类都无法充分描述清楚我们想要表达的问题,这种情况下可以创建自己的异常类,即自定义异常类。
2.自定义异常类只需从Exception类或者它的子类派生一个子类即可。
3.自定义异常类如果继承Exception类,则为受检查异常,必须对其进行处理;
如果不想处理,可以让自定义异常类继承运行时异常RuntimeException类
4.习惯上,自定义异常类应该包含2个构造器:一个是默认的构造器,另一个是带有详细信息的构造器。

【示例6-10】自定义异常类

/**IllegalAgeException:非法年龄异常,继承Exception类*/
class IllegalAgeException extends Exception {
    //默认构造器
    public IllegalAgeException() {
     
    }
    //带有详细信息的构造器,信息存储在message中
    public IllegalAgeException(String message) {
        super(message);
    }
}

【示例6-11】自定义异常类的使用

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.exit(-1);
        }
        System.out.println(p);
    }
}

执行结果如图6-11所示:
在这里插入图片描述
使用异常机制的建议
1.要避免使用异常处理代替错误处理,这样会降低程序的清晰性,并且效率低下。
2.处理异常不可以代替简单测试—只在异常情况下使用异常机制。
3.不要进行小粒度的异常处理—应该将整个任务包装在一个try语句块中。
4.异常往往在高层处理(先了解!后面做项目会说!) 。

6.7 如何利用百度解决异常问题

正常学习和开发中,我们经常会遇到各种异常。大家在遇到异常时,需要遵循下面四步来解决:
1.细心查看异常信息,确定异常种类和相关Java代码行号;
2.拷贝异常信息到百度,查看相关帖子,寻找解决思路;
3.前两步无法搞定,再问同学或同事;
4.前三步无法搞定,请示领导。

很多同学碰到异常一下就慌了,立刻开始请教别人搬救兵,殊不知这样做有两大坏处。第一、太不尊重别人,把别人当苦力。第二、失去提高自我的机会,自己解决一个异常,就意味着有能力解决一类异常。解决一类异常能大大提高自身能力。

本章总结

第六章 总结
1.Error与Exception都继承自Throwable类
2.Error类层次描述了Java运行时系统内部错误和资源耗尽错误。
3.Exception类是所有异常类的父类,其子类对应了各种各样可能出现的异常事件。
4.常见的异常类型
–ArithmeticException
–NullPointerException
–ClassCastException
–ArrayIndexOutOfBoundsException
–NumberFormatException
5.方法重写中声明异常原则:子类声明的异常范围不能超过父类声明的范围
6.异常处理的三种方式
–捕获异常:try-catch-finally
–声明异常:throws
7.自定义异常类只需从Exception类或者它的子类派生一个子类即可。

第九章 容器

9.1 泛型Generics

开发和学习中需要时刻和数据打交道,如何组织这些数据是我们编程中重要的内容。 我们一般通过“容器”来容纳和管理数据。那什么是“容器”呢?生活中的容器不难理解,是用来容纳物体的,如锅碗瓢盆、箱子和包等。程序中的“容器”也有类似的功能,就是用来容纳和管理数据。

事实上,我们第七章所学的数组就是一种容器,可以在其中放置对象或基本类型数据

数组的优势:是一种简单的线性序列,可以快速地访问数组元素,效率高。如果从效率和类型检查的角度讲,数组是最好的。
数组的劣势:不灵活。容量需要事先定义好,不能随着需求的变化而扩容。比如:我们在一个用户管理系统中,要把今天注册的所有用户取出来,那么这样的用户有多少个?我们在写程序时是无法确定的。因此,在这里就不能使用数组。

基于数组并不能满足我们对于“管理和组织数据的需求”,所以我们需要一种更强大、更灵活、容量随时可扩的容器来装载我们的对象。 这就是我们今天要学习的容器,也叫集合(Collection)。以下是容器的接口层次结构图:
在这里插入图片描述
为了能够更好的学习容器,我们首先要先来学习一个概念:泛型。

泛型是JDK1.5以后增加的,它可以帮助我们建立类型安全的集合。在使用了泛型的集合中,遍历时不必进行强制类型转换。JDK提供了支持泛型的编译器,将运行时的类型检查提前到了编译时执行,提高了代码可读性和安全性。

泛型的本质就是**“数据类型的参数化”。 我们可以把“泛型”理解为数据类型的一个占位符(形式参数),即告诉编译器,在调用泛型时必须传入实际类型。**

容器相当于一个大桶,到容器中放东西时。
泛型相当于在容器上贴个标签,规定每个容器中放什么东西。放的时候放什么,取得时候就取什么。

9.1.1 自定义泛型

我们可以在类的声明处增加泛型列表,如:<T,E,V>。
此处,字符可以是任何标识符,一般采用这3个字母。
例:不加泛型的时候
在这里插入图片描述
【示例9-1】泛型类的声明

class MyCollection<E> {// E:表示泛型;
	Object[] objs = new Object[5];

	public E get(int index) {// E:表示泛型;
		return (E) objs[index];
	}
	public void set(E e, int index) {// E:表示泛型;
		objs[index] = e;
	}
}

泛型E像一个占位符一样表示“未知的某个数据类型”,我们在真正调用的时候传入这个“数据类型”。
加了泛型,相当于提前就做好 类型 检查!
【示例9-2】泛型类的应用

public class TestGenerics {
	public static void main(String[] args) {
		// 这里的”String”就是实际传入的数据类型;
		MyCollection<String> mc = new MyCollection<String>();
		mc.set("aaa", 0);
		mc.set("bbb", 1);
		String str = mc.get(1); //加了泛型,直接返回String类型,不用强制转换;
		System.out.println(str);
	}
}

9.1.2 容器中使用泛型

容器相关类都定义了泛型,我们在开发和工作中,在使用容器类时都要使用泛型。这样,在容器的存储数据、读取数据时都避免了大量的类型判断,非常便捷。

【示例9-3】泛型类的在集合中的使用

public class Test {
	public static void main(String[] args) {
		// 以下代码中List、Set、Map、Iterator都是与容器相关的接口;
		List<String> list = new ArrayList<String>();
		Set<Man> mans = new HashSet<Man>();
		Map<Integer, Man> maps = new HashMap<Integer, Man>();
		Iterator<Man> iterator = mans.iterator();
	}
}

通过阅读源码,我们发现Collection、List、Set、Map、Iterator接口都定义了泛型,如下图所示:
在这里插入图片描述
因此,我们在使用这些接口及其实现类时,都要使用泛型。

菜鸟雷区:我们只是强烈建议使用泛型。事实上,不使用编译器也不会报错!

9.2 Collection接口

Collection 表示一组对象,它是集中、收集的意思。Collection接口的两个子接口是List、Set接口。
在这里插入图片描述
由于List、Set是Collection的子接口,意味着所有List、Set的实现类都有上面的方法。我们下一节中,通过ArrayList实现类来测试上面的方法。

9.3.1 List特点和常用方法

List是有序、可重复的容器。
有序:List中每个元素都有索引标记。可以根据元素的索引标记(在List中的位置)访问元素,从而精确控制这些元素。
可重复:List允许加入重复的元素。更确切地讲,List通常允许满足 e1.equals(e2) 的元素重复加入容器。

除了Collection接口中的方法,List多了一些跟顺序(索引)有关的方法,参见下表:
在这里插入图片描述

List接口常用的实现类有3个:ArrayList、LinkedList和Vector。
【示例9-4】List的常用方法

public class TestList {
    /**
     * 测试add/remove/size/isEmpty/contains/clear/toArrays等方法
     */
    public static void test01() {
        List<String> list = new ArrayList<String>();
        System.out.println(list.isEmpty()); // true,容器里面没有元素
        list.add("高淇");
        System.out.println(list.isEmpty()); // false,容器里面有元素
        list.add("高小七"); //存的是地址
        list.add("高小八");
        System.out.println(list);
        System.out.println("list的大小:" + list.size());
        System.out.println("是否包含指定元素:" + list.contains("高小七"));
        list.remove("高淇");//删的时候删的是地址,而人家本身对象还是存在的
        System.out.println(list);
        //转化成object数组
        Object[] objs = list.toArray();
        System.out.println("转化成Object数组:" + Arrays.toString(objs));
        
        list.clear();
        System.out.println("清空所有元素:" + list);
    }
    public static void main(String[] args) {
        test01();
    }
}

执行结果如图9-3所示:
在这里插入图片描述
【示例9-5】两个List之间的元素处理

public class TestList {
    public static void main(String[] args) {
        test02();
    }
    /**
     * 测试两个容器之间元素处理
     */
    public static void test02() {
        List<String> list = new ArrayList<String>();
        list.add("高淇");
        list.add("高小七");
        list.add("高小八");
 
        List<String> list2 = new ArrayList<String>();
        list2.add("高淇");
        list2.add("张三");
        list2.add("李四");
        System.out.println(list.containsAll(list2)); //false list是否包含list2中所有元素
        System.out.println(list);
        
        list.addAll(list2); //将list2中所有元素都添加到list中,list中的元素可以重复 是有序的
        System.out.println(list);
        
        list.removeAll(list2); //从list中删除同时在list和list2中存在的元素
        System.out.println(list);
        
        list.retainAll(list2); //取list和list2的交集
        System.out.println(list);
    }
}

执行结果如图9-4所示:
在这里插入图片描述
【示例9-6】List中操作索引的常用方法

public class TestList {
    public static void main(String[] args) {
        test03();
    }
    /**
     * 测试List中关于索引操作的方法
     */
    public static void test03() {
        List<String> list = new ArrayList<String>();
        list.add("A");
        list.add("B");
        list.add("C");
        list.add("D");
        System.out.println(list); // [A, B, C, D]
        list.add(2, "高");
        System.out.println(list); // [A, B, 高, C, D]
        list.remove(2);
        System.out.println(list); // [A, B, C, D]
        list.set(2, "c");
        System.out.println(list); // [A, B, c, D]
        System.out.println(list.get(1)); // 返回:B
        list.add("B");
        System.out.println(list); // [A, B, c, D, B]
        System.out.println(list.indexOf("B")); // 1 从头到尾找到第一个"B"
        System.out.println(list.lastIndexOf("B")); // 4 从尾到头找到第一个"B"
    }
}

执行结果如图9-5所示: 在这里插入图片描述

9.3.2 ArrayList特点和底层实现

ArrayList底层是用数组实现的存储。 特点:**查询效率高(ArrayList),增删效率低(LinkedList),线程不安全(Vector)。**我们一般使用它。查看源码:
在这里插入图片描述
我们可以看出ArrayList底层使用Object数组来存储元素数据。所有的方法,都围绕这个核心的Object数组来开展。

我们知道,数组长度是有限的,而ArrayList是可以存放任意数量的对象,长度不受限制,那么它是怎么实现的呢?
本质上就是通过定义新的更大的数组,将旧数组中的内容拷贝到新数组,来实现扩容
ArrayList的Object数组初始化长度为10,如果我们存储满了这个数组,需要存储第11个对象,就会定义新的长度更大的数组,并将原数组内容和新的元素一起加入到新数组中,源码如下:

在这里插入图片描述
右移1位,即除以2
在这里插入图片描述
删除,也是拷贝
在这里插入图片描述
用arraycopy

插入也是数组拷贝
在这里插入图片描述

手动实现ArrayList1_最简化方式——增加泛型

自定义实现一个ArrayList,体会底层原理
第一版
public class SxtArrayList{
private Object[] elementData;
private int size;

    private static final int DEFALT_CAPACITY=10;
   
public SxtArrayList(){
	elementData=new Object[DEFALT_CAPACITY];
}
public SxtArrayList(int capacity){
	elementData=new Object[capacity];
}

public void add(Object obj){
	elementData[size++]=obj;
}

public String toString(){
	StringBuilder sb=new StringBuilder ();
	//[a,b,c]
	sb.append("[");
	for(int i=0;i<size;i++){
		sb.append(elementData[i]+",")
	}
	sb.setCharAt(sb.length())-1,"]");
	return sb.toString();
}
public static void main(String[] args){
	SxtArrayList s1=new SxtArrayList (20);
	s1.add('aa');
	s1.add(”bb”);
	System.out.println(s1);//[aa,bb]
}

}

第二版
public class SxtArrayList{
private Object[] elementData;
private int size;

    private static final int DEFALT_CAPACITY=10;
   
public SxtArrayList(){
	elementData=new Object[DEFALT_CAPACITY];
}
public SxtArrayList(int capacity){
	elementData=new Object[capacity];
}

public void add(E element){
	elementData[size++]=obj;
}

public String toString(){
	StringBuilder sb=new StringBuilder ();
	//[a,b,c]
	sb.append("[");
	for(int i=0;i<size;i++){
		sb.append(elementData[i]+",")
	}
	sb.setCharAt(sb.length())-1,"]");
	return sb.toString();
}
public static void main(String[] args){
	SxtArrayList s1=new SxtArrayList (20);
	s1.add('aa');
	s1.add(”bb”);
	System.out.println(s1);//[aa,bb]
}

}

手动实现ArrayList1_数组扩容_debug调试

上次定义的是数组列表长度是10个
在这里插入图片描述
增加数组扩容
//什么时候扩容?
//怎么扩容?

public void add(E element){
	if(size==elementData.length){
		Object[] newArray=new Object[elementData.length+(elementData.length>>1)];//10+10/2
		System.arraycopy(elementData,0,newArray,0,elementData.length);//把原数组拷贝进了现在的数组
		elementData=newArray;

		elementData[size++]=element;
	}
}

在这里插入图片描述

手动实现ArrayList3_索引越界问题_get和set方法

public E get(int index){
	check(index);
	return (E)elementData[index];
}

public void set(E element,int index){
	//索引合法判断[0,size)
	if(index<0||index>size-1){
	throw new RuntimeException("索引不合法!"+index);
	}
	elementData[index]=element;
}
	
//加个索引越界方法,让别的用得到的方法去调用它
public void check(int index){
	//索引合法判断[0,size)
	if(index<0||index>size-1){
	throw new RuntimeException("索引不合法!"+index);
	}
}

在这里插入图片描述

手动实现ArrayList4_完善_remove两种实现

在这里插入图片描述
在这里插入图片描述

public void remove(E element){
	//element,将它和所有元素挨个比较,获得第一个比较为true的,返回
	for(int i=0;i<size;i++){
		if(element.equals(get(i))){//容器中所有的比较操作都用eauals而不是==
			//将该元素从此处移除
			remove(i);
		}
	}
}

public void remove(int index){
	//0 1 2 3 4 5 6 7
	//a,b,c,d,e,f,g ,h  8  3
	//a,b,c,e,f,g,h    7
	//相当于把后三位拷贝到了向前一位的位置
	int numMoved=elementData.length-index-1;
	if(numMoved>0){
		System.arraycopy(elementData,index+1,elementData,index,numMoved);
		elementData[size-1]=null;//这两行合并为一行elementData[--size]=null
		size--;
	}else{//其实这个else可以不要
		//最后一个,直接删掉
		elementData[size-1]=null;
		size--;
	}
}

public int size(){
	return size;
}

publuc boolean isEmpty(){
	return size==0?true:false;
}

9.3.3 LinkedList特点和底层实现

LinkedList底层用双向链表实现的存储。特点:查询效率低,增删效率高,线程不安全。

双向链表也叫双链表,是链表的一种,它的每个数据节点中都有两个指针,分别指向前一个节点和后一个节点。 所以,从双向链表中的任意一个节点开始,都可以很方便地找到所有节点。
在这里插入图片描述
每个节点都应该有3部分内容:

class  Node {
    Node  previous;     //前一个节点
    Object  element;    //本节点保存的数据
    Node  next;         //后一个节点
}

删除在这里插入图片描述
增加

我们查看LinkedList的源码,可以看到里面包含了双向链表的相关代码:
在这里插入图片描述
注意事项:entry在英文中表示“进入、词条、条目”的意思。在计算机英语中一般表示“项、条目”的含义。

手工实现LinkedList_节点概念_add方法

public class SxtLinkedList{
	private Node first;
	private Node last;
	private int size;
	//[]
	//["a"]
	//["a","b"]
	public void add(Object obj){
		Node node=new Node(obj);
		if(first==null){
			node.previous=null;
			node.next=null;
			first=node;
			last=node;
		}else{
			node.previous=last;
			node.next=null;
			last.next=node;
			last=node;
		}
	}
	
	public String toString(){
		//["a","b","c"]
		StringBulider sb=new StringBulider("[");
		Node temp=first;
		while(temp!=null){
			sb.append(temp.element+","];
			temp=temp.next;
		}
		sb.setCharAt(sb.length()-1,"]");
		return sb.toString();
		
	}

	public static void main(String[] args){
		SxtLinkedList list=new SxtLinkedList();
		list.add("a");
		list.add("b");
		list.add("c");
		System.out.println(list);//["a","b","c"]
	}
		 
}

public class Node{
	Node previous;
	Node next;
	Object element;
	public Node(Node previous,Node next;Object element){
		super();
		this.previous=previous;
		this.next=next;
		this.element=element;
	}
	public Node(Object element){
		super();
		this.element=element;
	}

手工实现LinkedList_get查询_节点遍历

public Object get(int index){
	//["a","b","c"]   2
	//first.next.next调用2次next
	if(index<0||index>size-1){
		throw new RuntimeException("索引数字不合法"+index);
	}
	Node temp=first;
	for(int i=0;i<index;i++){0   1
		temp=temp.next;
	}
	return temp.element;
}

System.out.println(list.get(2));//c

以上的写法太浪费时间

public Object get(int index){
	//["a","b","c"]   2
	//first.next.next调用2次next
	if(index<0||index>size-1){
		throw new RuntimeException("索引数字不合法"+index);
	}
	Node temp=null;

	if(index<=(size>>1)){//小于等于长度的一半,从前往后找
		temp=first;
		for(int i=0;i<index;i++){0   1
			temp=temp.next;
		}
	}else{
		temp=last;
		for(int i=size-1;i>index;i--){//从后往前找
			temp=temp.previous;
		}
	}		
	return temp.element;
}

手工实现LinkedList_remvoe移除节点

在这里插入图片描述

public Node getNode(int index){//封装一下查找节点
	Node temp=null;
    
    	if(index<=(size>>1)){//小于等于长度的一半,从前往后找
    		temp=first;
    		for(int i=0;i<index;i++){0   1
    			temp=temp.next;
    		}
    	}else{
    		temp=last;
    		for(int i=size-1;i>index;i--){//从后往前找
    			temp=temp.previous;
    		}
    	}		
 }



public void remove(int index){
	Node temp=getNode(index);
	if(temp!=null){
		Node up=temp.previous;
		Node down=temp.next;
		if(up!=null){
			up.next=down;
		}
		if(down!=null){
			down.privous=up;
		}
		if(index==0){//被删除的元素是第一个元素
			first=down;
		}
		if(index==size-1){//被删除的元素是最后一个元素
			last=up;
		}
		size--;
	}
}

手工实现LinkedList_插入节点

在这里插入图片描述
public void add(int index,Object obj){
Node newNode=new Node(obj);
Node temp=getNode(index);

if(temp!=null){
	Node up=temp.previous;
	up.next=newNode;
	newNode.previous=up;
	newNode.next=temp;
	temp.previous=newNode;

}

}

list.add(3,“ssds”);

手工实现LinkedList_完善_增加泛型

增加小的封装,增加泛型
public class SxtLinkedList{
private Node first;
private Node last;
private int size;
//[]
//[“a”]
//[“a”,“b”]
public void add(E element){
Node node=new Node(element);
if(first==null){
node.previous=null;
node.next=null;
first=node;
last=node;
}else{
node.previous=last;
node.next=null;
last.next=node;
last=node;
}
}

public void add(int index,E element){
	checkRange(index);
	Node newNode=new Node(element);
	Node temp=getNode(index);

	if(temp!=null){
		Node up=temp.previous;
		up.next=newNode;
		newNode.previous=up;
		newNode.next=temp;
		temp.previous=newNode;
	
	}
}
private void checkRange(int index){//所有用到索引的地方
	if(index<0||index>size-1){
	    		throw new RuntimeException("索引数字不合法"+index);
	}
}
	
	 public E get(int index){
    	//["a","b","c"]   2
    	//first.next.next调用2次next
    	checkRange(index);
    	Node temp=getNode(idex);
    
    	return temp!=null?(E)temp.element:null;
}

privateNode getNode(int index){//封装一下查找节点
		checkRange(index);
    		Node temp=null;
        
        	if(index<=(size>>1)){//小于等于长度的一半,从前往后找
        		temp=first;
        		for(int i=0;i<index;i++){0   1
        			temp=temp.next;
        		}
        	}else{
        		temp=last;
        		for(int i=size-1;i>index;i--){//从后往前找
        			temp=temp.previous;
        		}
        	}		
     }



public void remove(int index){
	checkRange(index);
	Node temp=getNode(index);
	if(temp!=null){
		Node up=temp.previous;
		Node down=temp.next;
		if(up!=null){
			up.next=down;
		}
		if(down!=null){
			down.privous=up;
		}
		if(index==0){//被删除的元素是第一个元素
			first=down;
		}
		if(index==size-1){//被删除的元素是最后一个元素
			last=up;
		}
		size--;
	}
}
	public String toString(){
		//["a","b","c"]
		StringBulider sb=new StringBulider("[");
		Node temp=first;
		while(temp!=null){
			sb.append(temp.element+","];
			temp=temp.next;
		}
		sb.setCharAt(sb.length()-1,"]");
		return sb.toString();
		
	}

	public static void main(String[] args){
		SxtLinkedList<String> list=new SxtLinkedList<>();
		list.add("a");
		list.add("b");
		list.add("c");
		System.out.println(list);//["a","b","c"]
	}
		 
}

public class Node{
	Node previous;
	Node next;
	Object element;
	public Node(Node previous,Node next;Object element){
		super();
		this.previous=previous;
		this.next=next;
		this.element=element;
	}
	public Node(Object element){
		super();
		this.element=element;
	}

9.3.4 Vector向量

Vector底层是用数组实现的List,相关的方法都加了同步检查,因此“线程安全,效率低”。 比如,indexOf方法就增加了synchronized同步标记
检查我们有没有获得对象锁
在这里插入图片描述
在这里插入图片描述

老鸟建议如何选用ArrayList、LinkedList、Vector?
1.需要线程安全时,用Vector。
2.不存在线程安全问题时,并且查找较多用ArrayList(一般使用它)。
3.不存在线程安全问题时,增加或删除元素较多用LinkedList。

9.4 Map接口

现实生活中,我们经常需要成对存储某些信息。比如,我们使用的微信,一个手机号只能对应一个微信账户。这就是一种成对存储的关系。
Map就是用来存储“键(key)-值(value) 对”的。 Map类中存储的“键值对”通过键来标识,所以“键对象”不能重复。
Map 接口的实现类有HashMap、TreeMap、HashTable、Properties等。
在这里插入图片描述
在这里插入图片描述
Map<Integer,String> m1=new HashMap<>();
m1.put(1,"one’);
m1.get(1);

9.4.1 HashMap和HashTable

HashMap采用哈希算法实现,是Map接口最常用的实现类。 由于底层采用了哈希表存储数据,我们要求键不能重复,如果发生重复,新的键值对会替换旧的键值对。 HashMap在查找、删除、修改方面都有非常高的效率。

【示例9-7】Map接口中的常用方法

public class TestMap {
    public static void main(String[] args) {
        Map<Integer, String> m1 = new HashMap<Integer, String>();
        Map<Integer, String> m2 = new HashMap<Integer, String>();
        m1.put(1, "one");
        m1.put(2, "two");
        m1.put(3, "three");
        m2.put(1, "一");
        m2.put(2, "二");
        System.out.println(m1.size());//3
        System.out.println(m1.containsKey(1));//true
        System.out.println(m2.containsValue("two"));//false
        m1.put(3, "third"); //键重复了,则会替换旧的键值对。重复是根据equals()判断的
        
        Map<Integer, String> m3 = new HashMap<Integer, String>();
        m3.putAll(m1);
        m3.putAll(m2);
        System.out.println("m1:" + m1);//m1:{1=one,2=two,3=third}
        System.out.println("m2:" + m2);//m2:{1=一,2=二}
        System.out.println("m3:" + m3);//m3:{1=一,2=二,3=third}
    }
}

执行结果如图9-11所示:
在这里插入图片描述

HashTable类和HashMap用法几乎一样,底层实现几乎一样,只不过HashTable的方法添加了synchronized关键字确保线程同步检查,效率较低。
HashMap与HashTable的区别
1.HashMap: 线程不安全,效率高。允许key或value为null。
2.HashTable: 线程安全,效率低。不允许key或value为null。

例:
class Employee{
private int id;
private String ename;
private double salary;
构造方法、get/set方法、tostring
}
public class TestMap2{
public static void main(String[] args){
Employee e1=new Employee(1001,“xiaosa”,500);
Employee e1=new Employee(1002,“xiaods”,500);
Employee e1=new Employee(1003,“xiaoafaf”,500);
Map<Integer,Emplyee> map=new HashMap<>();
map.put(1001,e1);
map.put(1002,e2);
map.put(1003,e3);
Employee emp=map.get(1001);
System.out.println(emp.getEname());
System.out.println(emp);
}
}

9.4.2 HashMap底层实现详解

HashMap底层实现采用了哈希表,这是一种非常重要的数据结构。对于我们以后理解很多技术都非常有帮助(比如:redis数据库的核心技术和HashMap一样),因此,非常有必要让大家理解。
数据结构中由数组和链表来实现对数据的存储,他们各有特点。
(1) 数组:占用空间连续。 寻址容易,查询速度快。但是,增加和删除效率非常低。
(2) 链表:占用空间不连续。 寻址困难,查询速度慢。但是,增加和删除效率非常高
那么,我们能不能结合数组和链表的优点(即查询快,增删效率也高)呢? 答案就是“哈希表”。 哈希表的本质就是“数组+链表”。

老鸟建议
对于本章中频繁出现的“底层实现”讲解,建议学有余力的童鞋将它搞通。刚入门的童鞋如果觉得有难度,可以暂时跳过。入门期间,掌握如何使用即可,底层原理是扎实内功,便于大家应对一些大型企业的笔试面试。

▪ Hashmap基本结构讲解
哈希表的基本结构就是“数组+链表”。我们打开HashMap源码,发现有如下两个核心内容:
在这里插入图片描述
在这里插入图片描述
其中的Entry[] table 就是HashMap的核心数组结构,我们也称之为“位桶数组”。我们再继续看Entry是什么,源码如下:
在这里插入图片描述
一个Entry对象存储了:
1.key:键对象 value:值对象
2.next:下一个节点
3.hash: 键对象的hash值
显然每一个Entry对象就是一个单向链表结构,我们使用图形表示一个Entry对象的典型示意:
在这里插入图片描述
然后,我们画出Entry[]数组的结构(这也是HashMap的结构):
在这里插入图片描述

▪ 存储数据过程put(key,value)
明白了HashMap的基本结构后,我们继续深入学习HashMap如何存储数据。此处的核心是如何产生hash值,该值用来对应数组的存储位置。
在这里插入图片描述
我们的目的是将”key-value两个对象”成对存放到HashMap的Entry[]数组中。参见以下步骤:
(1) 获得key对象的hashcode
首先调用key对象的hashcode()方法,获得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]的区间。 早期的HashTable就是采用这种算法。但是,这种算法由于使用了“除法”,效率低下。JDK后来改进了算法。首先约定数组长度必须为2的整数幂,这样采用
位运算**即可实现取余的效果:hash值 = hashcode&(数组长度-1)
iii. 如下为我们自己测试简单的hash算法:
【示例9-8】测试hash算法

public class Test {
    public static void main(String[] args) {
        int h = 25860399;
        int length = 16;//length为2的整数次幂,则h&(length-1)相当于对length取模
        myHash(h, length);
    }
    /**
     * @param h  任意整数
     * @param length 长度必须为2的整数幂
     * @return
     */
    public static  int myHash(int h,int length){
        System.out.println(h&(length-1));
        //length为2的整数幂情况下,和取余的值一样
        System.out.println(h%length);//取余数
        return h&(length-1);
    }
}

运行如上程序,我们就能发现**直接取余(h%length)位运算(h&(length-1))**结果是一致的。事实上,为了获得更好的散列效果,JDK对hashcode进行了两次散列处理(核心目标就是为了分布更散更均匀),源码如下:
在这里插入图片描述

(3) 生成Entry对象
如上所述,一个Entry对象包含4部分:key对象、value对象、hash值、指向下一个Entry对象的引用。我们现在算出了hash值。下一个Entry对象的引用为null。
(4) 将Entry对象放到table数组中
如果本Entry对象对应的数组索引位置还没有放Entry对象,则直接将Entry对象存储进数组;如果对应索引位置已经有Entry对象,则将已有Entry对象的next指向本Entry对象,形成链表。

总结如上过程:
当添加一个元素(key-value)时,首先计算key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,就形成了链表,同一个链表上的Hash值是相同的,所以说数组存放的是链表。 JDK8中,当链表长度大于8时,链表就转换为红黑树,这样又大大提高了查找的效率。

▪ 取数据过程get(key)
我们需要通过key对象获得“键值对”对象,进而返回value对象。明白了存储数据过程,取数据就比较简单了,参见以下步骤:
(1) 获得key的hashcode,通过hash()散列算法得到hash值,进而定位到数组的位置。
(2) 在链表上挨个比较key对象。 调用equals()方法,将key对象链表上所有节点的key对象进行比较,直到碰到返回true的节点对象为止。
(3) 返回equals()为true的节点对象的value对象
明白了存取数据的过程,我们再来看一下hashcode()和equals方法的关系:
Java中规定,两个内容相同(equals()为true)的对象必须具有相等的hashCode。因为如果equals()为true而两个对象的hashcode不同;那在整个存储过程中就发生了悖论。

▪ 扩容问题
HashMap的位桶数组,初始大小为16。实际使用时,显然大小是可变的。如果位桶数组中的元素达到(0.75*数组 length), 就重新调整数组大小变为原来2倍大小。
扩容很耗时。扩容的本质是定义新的更大的数组,并将旧数组内容挨个拷贝到新数组中。

▪ JDK8将链表在大于8情况下变为红黑二叉树
JDK8中,HashMap在存储一个元素时,当对应链表长度大于8时,链表就转换为红黑树,这样又大大提高了查找的效率。
下一节,我们简单介绍一个二叉树。同时,也便于大家理解TreeMap的底层结构。

手工实现HashMap_基本结构_put存储键值对

public class SxtHashMap{
	Node2[] table;//位桶数组 
	int size;//存放的键值对的个数
	public SxtHashMap{
		table=new Node2[16];//长度一般定义为2的整数幂
	}
	public void put(Object key,Object value){
		//定义了新的节点对象
		Node2 newNode=new Node2();
		newNode.hash=myHash(key.hashCode(),table.length);
		newNode.key=key;
		newNode.value=value;
		newNode.next=null;
		Node2 temp=table[newNode.hash];
		if(temp==null){
			table[newNdoe.hash]=newNode;//此处数组元素为空,则直接将新节点放进去
		}else{
			//此处数组元素不为空,则遍历对应链表
		}
		
	}
	public int myHash(int v,int length){
		return v&(length-1);//位运算
	}
	public  static void main(String[] args){
		SxtHashMap m1=new SxtHashMap ();
		m1.put(1,"one");
		m1.put(2,"two");
		m1.put(3,"three");
		System.out.println(m);
		
} 
	
public class Node2{
	int hash;
	Object  key;
	Object value;
	Node2 next;
}

手工实现HashMap_解决键重复问题_链表生成问题

public class SxtHashMap{
	Node2[] table;//位桶数组 
	int size;//存放的键值对的个数
	public SxtHashMap{
		table=new Node2[16];//长度一般定义为2的整数幂
	}
	public void put(Object key,Object value){
		//定义了新的节点对象
		Node2 newNode=new Node2();
		newNode.hash=myHash(key.hashCode(),table.length);
		newNode.key=key;
		newNode.value=value;
		newNode.next=null;
		
		Node2 temp=table[newNode.hash];
		Node iterLat=null;//正在遍历的最后一个元素
		boolean keyRepeat=false;//是否有重复
		if(temp==null){
			table[newNdoe.hash]=newNode;//此处数组元素为空,则直接将新节点放进去
			size++;
		}else{
			//此处数组元素不为空,则遍历对应链表
			while(temp!=null){
				//判断key如果重复则覆盖
				if(temp.key.eausls(key)){
					keyRepeat=true;
					temp.value=value;//只是覆盖value即可,其它的值保持不变
					break;
				}else{
    				//key不重复,则遍历下一个
    				iterLast=temp;
    				temp=temp.next;
    			}
    		}
    		if(keyRepeat){//如果key不重复,则添加到链表最后
    			iterLast,next=newNode;
    			size++;
    		}
		}
		
	}
	public int myHash(int v,int length){
		return v&(length-1);//位运算
	}
	public  static void main(String[] args){
		SxtHashMap m1=new SxtHashMap ();
		m1.put(1,"one");
		m1.put(2,"two");
		m1.put(3,"three");
		m1.put(3,"third");
		System.out.println(m);
		for(int i=10;i<100;i++){
			System.out.println(myHash(i,16));
		}
		
} 
	
public class Node2{
	int hash;
	Object  key;
	Object value;
	Node2 next;
}

手工实现HashMap_重写toString方法打印Map内容

方便查看map中的键值对信息
写个toString ,alt+?

public String toString(){
	//(10,aa,20:bb)
	StringBuilder sb=new StringBuilder("{");
	//遍历buket数组
	for(int i=0;i<table.length();i++){
		Node2 temp=table[i];
		//遍历链表
		while(temp!=null){
			sb.append(temp.key+":"+temp.value+",");
			temp=temp.next;
		}
	}
	sb.setCharAt(sb.length()-1,"}");
	return sb.toString();
}

手工实现HashMap_get查找键值对

根据键对象获得相应额值对象
put完善要考虑数组扩容问题

public Object get(Object key){{
	//获得相应的hash值,然后挨个比较
	int hash=myHash(key.hashCode(),table.length);
	Object value=null;
	if(table[hash]!=null){
		Node temp=table[hash];
		while(temp!=null){
			if(temp.key.equals(key)){
				value=temp.value;
				break;
			}else{
				temp=temp.next;
			}
		}
	}
	return value;
}

手工实现HashMap_完善封装_增加泛型

   public class SxtHashMap<K,V>{
    	Node2[] table;//位桶数组 
    	int size;//存放的键值对的个数
    	public SxtHashMap{
    		table=new Node2[16];//长度一般定义为2的整数幂
    	}
    	public void put(K key,V value){
    		//定义了新的节点对象
    		Node2 newNode=new Node2();
    		newNode.hash=myHash(key.hashCode(),table.length);
    		newNode.key=key;
    		newNode.value=value;
    		newNode.next=null;
    		
    		Node2 temp=table[newNode.hash];
    		Node iterLat=null;//正在遍历的最后一个元素
    		boolean keyRepeat=false;//是否有重复
    		if(temp==null){
    			table[newNdoe.hash]=newNode;//此处数组元素为空,则直接将新节点放进去
    			size++;
    		}else{
    			//此处数组元素不为空,则遍历对应链表
    			while(temp!=null){
    				//判断key如果重复则覆盖
    				if(temp.key.eausls(key)){
    					keyRepeat=true;
    					temp.value=value;//只是覆盖value即可,其它的值保持不变
    					break;
    				}else{
	    				//key不重复,则遍历下一个
	    				iterLast=temp;
	    				temp=temp.next;
	    			}
	    		}
	    		if(keyRepeat){//如果key不重复,则添加到链表最后
	    			iterLast,next=newNode;
	    			size++;
	    		}
    		}
    		
    	}
    	public int myHash(int v,int length){
    		return v&(length-1);//位运算
    	}
    	 public String toString(){
    	//(10,aa,20:bb)
    	StringBuilder sb=new StringBuilder("{");
    	//遍历buket数组
    	for(int i=0;i<table.length();i++){
    		Node2 temp=table[i];
    		//遍历链表
    		while(temp!=null){
    			sb.append(temp.key+":"+temp.value+",");
    			temp=temp.next;
    		}
    	}
    	sb.setCharAt(sb.length()-1,"}");
    	return sb.toString();
    }
    public V get(K key){{
    	//获得相应的hash值,然后挨个比较
    	int hash=myHash(key.hashCode(),table.length);
    	Vvalue=null;
    	if(table[hash]!=null){
    		Node temp=table[hash];
    		while(temp!=null){
    			if(temp.key.equals(key)){
    				value=(V)temp.value;
    				break;
    			}else{
    				temp=temp.next;
    			}
    		}
    	}
    	return value;
    }
    	public  static void main(String[] args){
    		SxtHashMap<Integer,String> m1=new SxtHashMap<> ();
    		m1.put(1,"one");
    		m1.put(2,"two");
    		m1.put(3,"three");
    		m1.put(3,"third");
    		System.out.println(m);
    		for(int i=10;i<100;i++){
    			System.out.println(myHash(i,16));
    		}
    		
    } 
    	
    public class Node<K,V>{
    	int hash;
    	K key;
    	V value;
    	Node2 next;
    }

9.4.3 二叉树和红黑二叉树

二叉树的定义
二叉树是树形结构的一个重要类型。 许多实际问题抽象出来的数据结构往往是二叉树的形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。
二叉树(BinaryTree)由一个节点及两棵互不相交的、分别称作这个根的左子树和右子树的二叉树组成。下图中展现了五种不同基本形态的二叉树。
在这里插入图片描述
(a) 为空树。
(b) 为仅有一个结点的二叉树。
© 是仅有左子树而右子树为空的二叉树。
(d) 是仅有右子树而左子树为空的二叉树。
(e) 是左、右子树均非空的二叉树。

注意事项
二叉树的左子树和右子树是严格区分并且不能随意颠倒的,图 © 与图 (d) 就是两棵不同的二叉树。

排序二叉树特性如下:
(1) 左子树上所有节点的值均小于它的根节点的值。
(2) 右子树上所有节点的值均大于它的根节点的值。
比如:我们要将数据【14,12,23,4,16,13, 8,3】存储到排序二叉树中,如下图所示:
在这里插入图片描述

排序二叉树本身实现了排序功能,可以快速检索。但如果插入的节点集本身就是有序的,要么是由小到大排列,要么是由大到小排列,那么最后得到的排序二叉树将变成普通的链表,其检索效率就会很差。 比如上面的数据【14,12,23,4,16,13, 8,3】,我们先进行排序变成:【3,4,8,12,13,14,16,23】,然后存储到排序二叉树中,显然就变成了链表,如下图所示:
在这里插入图片描述

平衡二叉树(AVL)
为了避免出现上述一边倒的存储,科学家提出了“平衡二叉树”。
在平衡二叉树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。 增加和删除节点可能需要通过一次或多次树旋转来重新平衡这个树。
节点的平衡因子是它的左子树的高度减去它的右子树的高度(有时相反)。带有平衡因子1、0或 -1的节点被认为是平衡的。带有平衡因子 -2或2的节点被认为是不平衡的,并需要重新平衡这个树。
比如,我们存储排好序的数据【3,4,8,12,13,14,16,23】,增加节点如果出现不平衡,则通过节点的左旋或右旋,重新平衡树结构,最终平衡二叉树如下图所示:
在这里插入图片描述
平衡二叉树追求绝对平衡,实现起来比较麻烦,每次插入新节点需要做的旋转操作次数不能预知。

▪ 红黑二叉树
红黑二叉树(简称:红黑树),它首先是一棵二叉树,同时也是一棵自平衡的排序二叉树
红黑树在原有的排序二叉树增加了如下几个要求:
1.每个节点要么是红色,要么是黑色。
2.根节点永远是黑色的。
3.所有的叶节点都是空节点(即 null),并且是黑色的。
4.每个红色节点的两个子节点都是黑色。(从每个叶子到根的路径上不会有两个连续的红色节点)
5.从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。
这些约束强化了红黑树的关键性质:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。这样就让树大致上是平衡的。
红黑树是一个更高效的检索二叉树,JDK 提供的集合类 TreeMap、TreeSet 本身就是一个红黑树的实现。
在这里插入图片描述
红黑树的基本操作:插入、删除、左旋、右旋、着色。 每插入或者删除一个节点,可能会导致树不在符合红黑树的特征,需要进行修复,进行 “左旋、右旋、着色”操作,使树继续保持红黑树的特性。

老鸟建议
本节关于二叉树的介绍,仅限于了解。实际开发中,直接用到的概率非常低。普通企业面试中也较少。不过,极有可能出现在BAT等企业笔试中。建议,想进BAT等名企的童鞋,专门准备一下数据结构相关的知识。

9.4.4 TreeMap的使用和底层实现

TreeMap是红黑二叉树的典型实现。我们打开TreeMap的源码,发现里面有一行核心代码:

private transient Entry<K,V> root = null;

root用来存储整个树的根节点。我们继续跟踪Entry(是TreeMap的内部类)的代码:
在这里插入图片描述
可以看到里面存储了本身数据、左节点、右节点、父节点、以及节点颜色。 TreeMap的put()/remove()方法大量使用了红黑树的理论。本书限于篇幅,不再展开。需要了解更深入的,可以参考专门的数据结构书籍。
TreeMap和HashMap实现了同样的接口Map,因此,用法对于调用者来说没有区别。HashMap效率高于TreeMap;在需要排序的Map时才选用TreeMap。

public class TestTreeMap{
	public static void main(String[] args){
		Map<Integer,String> treemap1=new TreeMap<>();
		treemap1.put(20,"aa");
		treemap1.put(3,"bb");
		treemap1.put(6,"cc");
		for(Integer key:treemap1.keySet()){
			System.out.println(key+"....."+treemap1.get(key));//按照key递增的方式排序
		}
		Map<Employee,String> treemap2=new TreeMap<>();
		treemap2.put(new Employee(100,"张三",5000),"张三是一个好小伙");
		treemap2.put(new Employee(200,"李四",500),"李四工作不积极");
		treemap2.put(new Employee(150,"王五",6000),"王五工作还不错");
		for(Emp key:treemap2.keySet()){
			System.out.println(key+"....."+treemap2.get(key));
		}
	}
}
//自定义的类该如何排序呢?
class Employee iplements Comparable<Employee >{
	int id;
	String name;
	double salary;
	构造,getset,tosring
	public int compareTo(Employee o){//负数:小于  0:等于  正数:大于
		if(this.salary>o.salary){
			return 1;
		}else if(this.salary<o.salary){
			return -1;
		}else{
			if(this.id>o.id){
				return 1;
			}else if(this.id<o.id){
				return -1;
			}else{
				return 0;
			}
		}
	}

9.5 Set接口

Set接口继承自Collection,Set接口中没有新增方法,方法和Collection保持完全一致。我们在前面通过List学习的方法,在Set中仍然适用。因此,学习Set的使用将没有任何难度。

Set容器特点:无序、不可重复
无序指Set中的元素没有索引,我们只能遍历查找;
不可重复指不允许加入重复的元素。
更确切地讲,新元素如果和Set中某个元素通过equals()方法对比为true,则不能加入;甚至,Set中也只能放入一个null元素,不能多个。

Set常用的实现类有:HashSet、TreeSet等,我们一般使用HashSet。

9.5.1 HashSet基本使用

大家在做下面练习时,重点体会“Set是无序、不可重复”的核心要点。

【示例9-9】HashSet的使用

public class Test {
    public static void main(String[] args) {
        Set<String> s = new HashSet<String>();
        s.add("hello");
        s.add("world");
        System.out.println(s);
        
        s.add("hello"); //相同的元素不会被加入
        System.out.println(s);
        
        s.add(null);
        System.out.println(s);
        
        s.add(null);
        System.out.println(s);
    }
}

执行结果如图9-24所示:
在这里插入图片描述

9.5.2 HashSet底层实现

HashSet是采用哈希算法实现,底层实际是用HashMap实现的(HashSet本质就是一个简化版的HashMap),因此,查询效率和增删效率都比较高。我们来看一下HashSet的源码:
在这里插入图片描述
我们发现里面有个map属性,这就是HashSet的核心秘密。我们再看add()方法,发现增加一个元素说白了就是在map中增加一个键值对,键对象就是这个元素,值对象是名为PRESENT的Object对象。说白了,就是“往set中加入元素,本质就是把这个元素作为key加入到了内部的map中”。

public class SxtHashSet{
	HashMap map;
	private static final Object PRESENT=new Object();
	public SxtHashSet(){
		map=new HashMap();
	}
	public int size(){
		return map.size();
	}
	public voud add(Object o){
		map.put(o,PRSENT);
	}
}

由于map中key都是不可重复的,因此,Set天然具有“不可重复”的特性。

9.5.3 TreeSet的使用和底层实现

TreeSet底层实际是用TreeMap实现的,内部维持了一个简化版的TreeMap,通过key来存储Set的元素。 TreeSet内部需要对存储的元素进行排序,因此,我们对应的类需要实现Comparable接口。这样,才能根据compareTo()方法比较对象之间的大小,才能进行内部排序。

【示例9-10】TreeSet和Comparable接口的使用

public class Test {
    public static void main(String[] args) {
        User u1 = new User(1001, "高淇", 18);
        User u2 = new User(2001, "高希希", 5);
        Set<User> set = new TreeSet<User>();
        set.add(u1);
        set.add(u2);
    }
}
 
class User implements Comparable<User> {
    int id;
    String uname;
    int age;
 
    public User(int id, String uname, int age) {
        this.id = id;
        this.uname = uname;
        this.age = age;
    }
    /**
     * 返回0 表示 this == obj 返回正数表示 this > obj 返回负数表示 this < obj
     */
    @Override
    public int compareTo(User o) {
        if (this.id > o.id) {
            return 1;
        } else if (this.id < o.id) {
            return -1;
        } else {
            return 0;
        }
    }
}

使用TreeSet要点:
(1) 由于是二叉树,需要对元素做内部排序。 如果要放入TreeSet中的类没有实现Comparable接口,则会抛出异常:java.lang.ClassCastException。
(2) TreeSet中不能放入null元素。

9.6.2 使用Iterator迭代器遍历容器元素(List/Set/Map)

迭代器为我们提供了统一的遍历容器的方式,参见以下示例代码:
在这里插入图片描述
有没有下一个元素
获得下一个元素
for (Iterator iter = aList.iterator(); iter.hasNext()? {
iter.next();
【示例9-11】迭代器遍历List

public class Test {
    public static void main(String[] args) {
        List<String> aList = new ArrayList<String>();
        for (int i = 0; i < 5; i++) {
            aList.add("a" + i);
        }
        System.out.println(aList);
        for (Iterator<String> iter = aList.iterator(); iter.hasNext();) {
            String temp = iter.next();
            System.out.print(temp + "\t");
            if (temp.endsWith("3")) {// 删除3结尾的字符串
                iter.remove();
            }
        }
        System.out.println();
        System.out.println(aList);
    }
}

执行结果如图9-27所示:
在这里插入图片描述
老鸟建议:如果遇到遍历容器时,判断删除元素的情况,使用迭代器遍历!

【示例9-12】迭代器遍历Set

public class Test {
    public static void main(String[] args) {
        Set<String> set = new HashSet<String>();
        for (int i = 0; i < 5; i++) {
            set.add("a" + i);
        }
        System.out.println(set);
        for (Iterator<String> iter = set.iterator(); iter.hasNext();) {
            String temp = iter.next();
            System.out.print(temp + "\t");
        }
        System.out.println();
        System.out.println(set);
    }
}

执行结果如图9-28所示:
在这里插入图片描述
【示例9-13】迭代器遍历Map一

public class Test {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<String, String>();
        map.put("A", "高淇");
        map.put("B", "高小七");
        Set<Entry<String, String>> ss = map.entrySet();
        for (Iterator<Entry<String, String>> iterator = ss.iterator(); iterator.hasNext();) {
            Entry<String, String> e = iterator.next();
            System.out.println(e.getKey() + "--" + e.getValue());
        }
    }
}

执行结果如图9-29所示:
在这里插入图片描述
我们也可以通过map的keySet()、valueSet()获得key和value的集合,从而遍历它们。

【示例9-14】迭代器遍历Map二

public class Test {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<String, String>();
        map.put("A", "高淇");
        map.put("B", "高小七");
        Set<String> ss = map.keySet();
        for (Iterator<String> iterator = ss.iterator(); iterator.hasNext();) {
            String key = iterator.next();
            System.out.println(key + "--" + map.get(key));
        }
    }
}

执行结果如图9-30所示:
在这里插入图片描述

9.7 遍历集合的方法总结

【示例9-15】遍历List方法一:普通for循环

for(int i=0;i<list.size();i++){//list为集合的对象名
    String temp = (String)list.get(i);
    System.out.println(temp);
}

【示例9-16】遍历List方法二:增强for循环(使用泛型!)

for (String temp : list) {
System.out.println(temp);
}

【示例9-17】遍历List方法三:使用Iterator迭代器(1)

for(Iterator iter= list.iterator();iter.hasNext();){
    String temp = (String)iter.next();
    System.out.println(temp);
}

【示例9-18】遍历List方法四:使用Iterator迭代器(2)

Iterator  iter =list.iterator();
while(iter.hasNext()){
    Object  obj =  iter.next();
    iter.remove();//如果要遍历时,删除集合中的元素,建议使用这种方式!
    System.out.println(obj);
}

【示例9-19】遍历Set方法一:增强for循环

for(String temp:set){
System.out.println(temp);
}

【示例9-20】遍历Set方法二:使用Iterator迭代器

for(Iterator iter = set.iterator();iter.hasNext();){
    String temp = (String)iter.next();
    System.out.println(temp);
}

【示例9-21】遍历Map方法一:根据key获取value

Map<Integer, Man> maps = new HashMap<Integer, Man>();
Set<Integer>  keySet =  maps.keySet();
for(Integer id : keySet){
System.out.println(maps.get(id).name);
}

【示例9-22】遍历Map方法二:使用entrySet

Set<Entry<Integer, Man>>  ss = maps.entrySet();
for (Iterator iterator = ss.iterator(); iterator.hasNext();) {
    Entry e = (Entry) iterator.next(); 
    System.out.println(e.getKey()+"--"+e.getValue());

9.8 Collections工具类

类 java.util.Collections 提供了对Set、List、Map进行排序、填充、查找元素的辅助方法。
1.void sort(List) //对List容器内的元素排序,排序的规则是按照升序进行排序。
2.void shuffle(List) //对List容器内的元素进行随机排列。
3.void reverse(List) //对List容器内的元素进行逆续排列 。
4.void fill(List, Object) //用一个特定的对象重写整个List容器。
5.int binarySearch(List, Object)//对于顺序的List容器,采用折半查找的方法查找特定对象。
【示例9-23】Collections工具类的常用方法

public class Test {
    public static void main(String[] args) {
        List<String> aList = new ArrayList<String>();
        for (int i = 0; i < 5; i++){
            aList.add("a" + i);
        }
        System.out.println(aList);
        
        Collections.shuffle(aList); // 随机排列
        System.out.println(aList);
        Collections.reverse(aList); // 逆续
        System.out.println(aList);
        Collections.sort(aList); // 排序
        System.out.println(aList);
        System.out.println(Collections.binarySearch(aList, "a2")); 
        Collections.fill(aList, "hello");
        System.out.println(aList);
    }
}

执行结果如图9-31所示:
在这里插入图片描述

表格数据存储_map和list结合存储整张表

在这里插入图片描述
方式一:每一行数据使用一个map,整个表格使用一个list

public class TestStoreData{
//map表示一行数据,多行数据是多个map,将多个map放到list中
	public static void main(String[] args){
		Map<String,Object> row1=new HashMap<>();
		row1.put("id",1001);
		row1.put("name","张三");
		row1.put("薪水",2000);
		row1.put("入职日期",2018.5.5);
		
		Map<String,Object> row2=new HashMap<>();
		row2.put("id",1002);
		row2.put("name","李四");
		row2.put("薪水",3000);
		row2.put("入职日期",2005.5.5);

		List<Map<String,Object>> table1=new ArrayList<>();
		for(Map<String,Object> row:table1){
			Set<String> keyset=row.keySet();
			for(String key:keyset){
				System.out.println(key+":"+row.get(key));
			}
		}
	}

表格数据存储_javabean和list结合存储整张表

每一行数据使用一个javabean对象,整个表格使用一个list
public class TestStoreData2{
public static void main(String[] args){
User user1=new User(1001,"张三”,20000,“2018.5.5”);
User user2=new User(1001,"李四”,30000,“2005.5.5”);
User user3=new User(1001,"王五”,40000,“2000.5.5”);
List list=new ArrayList<>();
list.add(user1);
list.add(user2);
list.add(user3);
for(User u:list){
System.out.println(u);
}

	Map<Integer,User> map=new HashMap<>();
	map.put(1001,user1);
	map.put(1002,user2);
	map.put(1003,user3);
	Set<Integer> keyset=map.keySet();
	for(Integer key:ketSet){
		System.out.println(key+"==="+map.get(key));
	}

}

class User{
private int id;
private String name;
private double salary;
private String hiredate;
构造方法、setget、tostring
}

本章总结

1.Collection 表示一组对象,它是集中、收集的意思,就是把一些数据收集起来。
2.Collection接口的两个子接口:

  1. List中的元素有顺序,可重复。常用的实现类有ArrayList、LinkedList和 vector。
    Ø ArrayList特点:查询效率高,增删效率低,线程不安全。
    Ø LinkedList特点:查询效率低,增删效率高,线程不安全。
    Ø vector特点:线程安全,效率低,其它特征类似于ArrayList。
  2. Set中的元素没有顺序,不可重复。常用的实现类有HashSet和TreeSet。
    Ø HashSet特点:采用哈希算法实现,查询效率和增删效率都比较高。
    Ø TreeSet特点:内部需要对存储的元素进行排序。因此,我们对应的类需要实现Comparable接口。这样,才能根据compareTo()方法比较对象之间的大小,才能进行内部排序。
    3.实现Map接口的类用来存储键(key)-值(value) 对。Map 接口的实现类有HashMap和TreeMap等。Map类中存储的键-值对通过键来标识,所以键值不能重复。
    4.Iterator对象称作迭代器,用以方便的实现对容器内元素的遍历操作。
    5.类 java.util.Collections 提供了对Set、List、Map操作的工具方法。
    6.如下情况,可能需要我们重写equals/hashCode方法:
  3. 要将我们自定义的对象放入HashSet中处理。
  4. 要将我们自定义的对象作为HashMap的key处理。
  5. 放入Collection容器中的自定义对象后,可能会调用remove、contains等方法时。
    7.JDK1.5以后增加了泛型。泛型的好处:
  6. 向集合添加数据时保证数据安全。
  7. 遍历集合元素时不需要强制转换。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值