11【泛型、双列集合、异常】
一、泛型
1.1 泛型概述
泛型定义:把类型明确的工作延迟到创建对象或调用方法的时候才去明确的特殊的类型;
例如,我们知道集合是可以存储任意元素的,那么这样一想,add方法上的参数应该是Object(所有类的父类),但是这样会引入一个新的问题,我们知道,子类都是比父类强大的,我们在使用的时候肯定是希望获取的是当初存进去的具体子类对象;因此我们每次都需要进行强制转换;
但add方法真的是Object吗?
查看ArrayList的add方法:
class ArrayList<E>{
public boolean add(E e){ }
public E get(int index){ }
....
}
Collection类:
public interface Collection<E> extends Iterable<E> {
}
上面的E就是泛型,集合的定义者也不知道我们需要存储什么元素到集合中,具体的类型只能延迟到创建对象时来决定了;
1.2 集合泛型的使用
1.2.1 未使用泛型
定义一个Province对象:
package com.dfbz.demo01;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Province {
private String name; // 名称
private String shortName; // 简称
private String location; // 所属区域
public void intro() {
System.out.println("名称: " + name + ",简称: " + shortName + ",所属地区: " + location);
}
@Override
public String toString() {
return "Province{" +
"name='" + name + '\'' +
", shortName='" + shortName + '\'' +
", location='" + location + '\'' +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getShortName() {
return shortName;
}
public void setShortName(String shortName) {
this.shortName = shortName;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public Province() {
}
public Province(String name, String shortName, String location) {
this.name = name;
this.shortName = shortName;
this.location = location;
}
}
测试类:
package com.dfbz.demo01;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo01_泛型问题引出 {
public static void main(String[] args) {
Collection list = new ArrayList();
// 往集合存储对象
list.add(new Province("台湾", "台","华东"));
list.add(new Province("澳门", "澳","华南"));
list.add(new Province("香港", "港","华南"));
list.add(new Province("河北", "冀","华北"));
// 获取迭代器
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
// 集合中的元素都被提升为Object对象了
Object obj = iterator.next();
// 强制转换为子类
Province province = (Province) obj;
// 调用子类特有的功能
province.intro();
}
}
}
我们没有给泛型进行明确的定义,对象存储到集合中都被提升为Object类型了,取出来时需要强制转换为具体子类,非常麻烦;
不仅如此,这样的代码还存在这隐藏的风险,集合中可以存储任意的对象,如果往集合中存储其他对象呢?
定义个Book对象:
package com.dfbz.demo01;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Book {
private String name;
private String author;
public void detail() {
System.out.println("书名: " + name + ",作者: " + author);
}
@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", author='" + author + '\'' +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public Book() {
}
public Book(String name, String author) {
this.name = name;
this.author = author;
}
}
测试类:
package com.dfbz.demo01;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo02_泛型出现的问题 {
public static void main(String[] args) {
Collection list = new ArrayList();
// 添加几个省份
list.add(new Province("山西", "晋","华北"));
list.add(new Province("河南", "豫","华中"));
list.add(new Province("江西", "赣","华东"));
// 添加几本书
list.add(new Book("《史记》","司马迁"));
list.add(new Book("《三国志》","陈寿"));
// 获取迭代器
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
// 集合中的元素都被提升为Object对象了
Object obj = iterator.next();
// 强制转换为子类
Province province = (Province) obj; // 不可以将Book转换为Province(存在隐藏问题)
// 调用子类特有的功能
province.intro();
}
}
}
运行结果:
上述代码编译时是没有什么问题,但运行时出现了类型转换异常:ClassCastException
,代码存在一定的安全隐患;
1.2.2 使用泛型
- 查看List源码:
public interface List<E> extends Collection<E> {
boolean add(E e);
....
}
集合的定义者发现,无法在定义集合类时就确定该集合存储的具体类型,因此使用泛型进行占位,使用者创建集合时明确该泛型的类型;
- 指定泛型后:
class List<Province>{
boolean add(Province province);
....
}
Tips:在创建对象时指定泛型的类型,泛型一旦指定了具体的类型,原来泛型的占位符(E),都将变为此类型;
如何指定泛型类类型?
格式如下:
List<泛型的具体类型> list=new ArrayList();
测试类:
package com.dfbz.demo01;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo03_使用泛型来存储元素 {
public static void main(String[] args) {
// 创建集合,并确定好泛型(存储的类型)
List<Province> list = new ArrayList();
// 往集合存储对象
list.add(new Province("甘肃", "甘|陇","西北"));
list.add(new Province("陕西", "陕|秦","西北"));
list.add(new Province("贵州", "贵|黔","西南"));
list.add(new Province("云南", "云|滇","西南"));
list.add(new Province("四川", "川|蜀","西南"));
// 获取City类型的迭代器
Iterator<Province> iterator = list.iterator();
while (iterator.hasNext()){
// 直接获取province类型
Province province = iterator.next();
province.intro();
}
}
}
1.3 泛型类
1.3.1 泛型类的使用
很明显,Collection、List、Set以及其下的子类都是泛型类,我们根据使用情况也可以定义泛型类;让泛型类的类型延迟到创建对象的时候指定;
- 使用格式:
修饰符 class 类名<代表泛型的变量> {}
例如,API中的List接口:
class List<E>{
boolean add(E e);
....
}
在创建对象的时候确定泛型
例如,List<String> list = new ArrayList<String>();
此时,变量E的值就是String类型,那么我们的类型就可以理解为:
public interface List<String> extends Collection<String> {
boolean add(String e);
...
}
再例如,ArrayList<Integer> list = new ArrayList<Integer>();
此时,变量E的值就是Integer类型,那么我们的类型就可以理解为:
public interface List<Integer> extends Collection<Integer> {
boolean add(Integer e);
...
}
举例自定义泛型类:
package com.dfbz.demo01;
public class GetClass<P> {
// 使用泛型类型,P具体的类型还不明确,等到创建GetClass对象时再明确P的类型
private P p;
public P getC() {
return p;
}
public void setC(P p) {
this.p = p;
}
}
使用:
package com.dfbz.demo01;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo04_自定义泛型类 {
public static void main(String[] args) {
// 创建对象,并明确泛型类型为City
GetClass<Province> getClass = new GetClass<>();
// setC(Province province)
getClass.setC(new Province("新疆","新","西北"));
// Province getC()
Province province = getClass.getC();
province.intro();
}
}
1.2.2 泛型类的继承
定义一个父类,带有泛型:
public class 类名<泛型类型> { }
例如,
package com.dfbz.demo01;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Fu<E> {
public void show(E e) {
System.out.println(e.toString());
}
}
使用格式:
- 1)定义类时确定泛型的类型
例如
package com.dfbz.demo01;
/**
* @author lscl
* @version 1.0
* @intro: 在定义子类时就确定好泛型的类型
*/
public class Zi1 extends Fu<Province> {
@Override
public void show(Province province) {
System.out.println("zi1: " + province);
}
}
此时,泛型E的值就是Province类型。
- 2)始终不确定泛型的类型,直到创建对象时,确定泛型的类型
例如
package com.dfbz.demo01;
/**
* @author lscl
* @version 1.0
* @intro: 子类也变为泛型类,泛型类型具体到创建对象的时候再确定
*/
public class Zi2<P> extends Fu<P> {
@Override
public void show(P p) {
System.out.println("zi2: " + p);
}
}
确定泛型:
package com.dfbz.demo01;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo05_测试泛型类的继承 {
public static void main(String[] args) {
// 注意: Zi1类以及不是一个泛型类了,不能够指定泛型
// Zi1<Book> zi1=new Zi1(); // 报错
// 创建子类时确定泛型类型
Zi2<Province> zi_a = new Zi2<>();
zi_a.show(new Province("青海", "青", "西北"));
Zi2<Book> zi_b = new Zi2<>();
zi_b.show(new Book("《说岳全传》", "钱彩"));
}
}
1.4 泛型方法
泛型类是在创建类时定义泛型类型,在创建对象时确定泛型类型;泛型方法则是在创建方法是定义泛型类型,在调用方法时确定泛型类型;
- 定义格式:
修饰符 <代表泛型的变量> 返回值类型 方法名(参数){}
例如:
package com.dfbz.demo01;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class GetClassMethod {
// 定义泛型方法
public <P> P GetClass(P p) {
return p;
}
}
使用格式:调用方法时,确定泛型的类型
package com.dfbz.demo01;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo06_测试泛型方法 {
public static void main(String[] args) {
GetClassMethod getClassMethod = new GetClassMethod();
// Province getClass(Province province)
Province province = getClassMethod.GetClass(new Province("西藏", "藏", "西北"));
province.intro();
// Book getClass(Book book)
Book book = getClassMethod.GetClass(new Book("《天龙八部》", "金庸"));
book.detail();
}
}
1.5 泛型通配符
1.5.1 通配符的使用
1)参数列表带有泛型
泛型在程序运行中全部会被擦除,我们把这种现象称为泛型擦除;但编译时期在使用泛型类进行方法传参时,不仅要匹配参数本身的类型,还要匹配泛型的类型;
Number是所有数值类的父类:
- 定义一个泛型类:
package com.dfbz.泛型通配符;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class TestClass<E> {
private E e;
public E getE() {
return e;
}
public void setE(E e) {
this.e = e;
}
}
- 测试带有泛型的方法参数列表:
package com.dfbz.泛型通配符;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo01_方法参数带有泛型的问题 {
public static void main(String[] args) {
TestClass<Number> t1 = new TestClass<>();
test1(t1);
TestClass<Integer> t2 = new TestClass<>();
// 符合方法的参数类型(TestClass),但不符合泛型类型(Number),编译报错
// test1(t2);
}
public static void test1(TestClass<Number> testClass) {
// 可以获取具体的泛型对象
Number number = testClass.getE();
System.out.println("test1...");
}
/*
和上面的方法冲突了,因为泛型在运行期间会被擦除
相当于: public static void test1(TestClass testClass)
*/
/*
public static void test1(TestClass<Integer> testClass) {
System.out.println("test1...");
}
*/
}
2)泛型通配符
泛型的通配符:不知道使用什么类型来接收的时候,此时可以使用?,?表示未知通配符。
- 示例:
package com.dfbz.泛型通配符;
import com.dfbz.demo01.Book;
import com.dfbz.demo01.Province;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo02_泛型通配符的使用 {
public static void main(String[] args) {
TestClass<String> t1 = new TestClass<>();
test1(t1);
TestClass<Integer> t2 = new TestClass<>();
test1(t2);
TestClass<Number> t3 = new TestClass<>();
test1(t3);
TestClass<Province> t4 = new TestClass<>();
test1(t4);
TestClass<Book> t5 = new TestClass<>();
test1(t5);
}
// ?: 代表可以接收任意泛型
public static void test1(TestClass<?> testClass) {
// 被?接收的泛型都将提升为Object
Object obj = testClass.getE();
System.out.println("test1...");
}
}
Tips:需要注意的是,被<?>接收过的类型都将提升为Object类型;
1.5.2 泛型上下边界
利用泛型通配符?
可以接收任意泛型,但是随之而然带来一个问题,就是所有类型都提升为Object;范围太广了,使用起来非常不方便,因此为了让泛型也可以利用多态的特点,泛型的上下边界的概念由此引出;
利用泛型的通配符可以指定泛型的边界;
泛型的上限:
- 格式:
类型名称 <? extends 类> 对象名称
- 含义:
只能接收该类型及其子类
泛型的下限:
- 格式:
类型名称 <? super 类> 对象名称
- 含义:
只能接收该类型及其父类型
可以看到基本数据类型的包装类都是继承与Number类;
- 测试泛型上下边界:
package com.dfbz.泛型通配符;
import java.util.ArrayList;
import java.util.Collection;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo03_泛型的上下边界 {
public static void main(String[] args) {
Collection<Integer> list1 = new ArrayList<Integer>();
Collection<String> list2 = new ArrayList<String>();
Collection<Number> list3 = new ArrayList<Number>();
Collection<Object> list4 = new ArrayList<Object>();
getElement1(list1); // Integer 是Number类型的子类
// getElement1(list2); // 报错,String不是Number类型的子类
getElement1(list3); // Number类型可以传递
// getElement1(list4); // 报错,Object不是Number类型的子类
// getElement2(list1); // 报错,Integer不是Number类型的父类
// getElement2(list2); // 报错,String不是Number类型的父类
getElement2(list3); // Number类型可以传递
getElement2(list4); // Object是Number类型的父类
}
// 泛型的上限:此时的泛型?,必须是Number类型或者Number类型的子类
public static void getElement1(Collection<? extends Number> coll) {
}
// 泛型的下限:此时的泛型?,必须是Number类型或者Number类型的父类
public static void getElement2(Collection<? super Number> coll) {
}
}
1.6 泛型的擦除
Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程称为类型擦除。
1.6.1 限制擦除
泛型在擦除过程中有限制擦除和无限制擦除;
- 有限制擦除:
- 无限制擦除:将泛型类型提升为Object类型
1.6.2 泛型的桥接方法
在接口使用泛型时,在运行期间接口中的泛型也一样会被擦除;
那如果编写了实现类来实现这个泛型接口,实现类中的泛型在运行期间也会被擦除,这样一来就会出现接口的方法并没有在实现类中得到实现:
好在JVM进行了特殊处理,如果我们编写的类实现了一个带有泛型的接口时,在运行期期间JVM会在实现类中帮我们自动的生产一个方法来帮助我们实现泛型接口中被擦除过后的那个方法,这个方法被称为桥接方法;
- 如图所示:
二、Map双列集合
2.1 Map集合概述
Map 用于保存具有映射关系的数据,如一个学生的ID对应一个学生,一个商品的ID对应一个商品,一个部门ID对应多个员工;这种具有对应关系的数据成为映射;
因此 Map 集合里保存着两组值,一组值用于保存 Map 里的 Key,另外一组用于保存 Map 里的 Value,Map 中的 key 和 value 都可以是任何引用类型的数据;
- Map接口的继承体系:
Tips:Collection接口是单列集合的顶层接口,Map则是双列集合的顶层接口;
2.2 Map接口的共有方法
Map是所有双列集合的顶层父类,因此Map中具备的是所有双列集合的共性方法;常用的方法如下:
public V put(K key, V value)
: 把指定的键与指定的值添加到Map集合中。public V remove(Object key)
: 把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值。public V get(Object key)
根据指定的键,在Map集合中获取对应的值。boolean containsKey(Object key)
判断集合中是否包含指定的键。public Set<K> keySet()
: 获取Map集合中所有的键,存储到Set集合中。public Set<Map.Entry<K,V>> entrySet()
: 获取到Map集合中所有的键值对对象的集合(Set集合)。public Collection<V> values()
:获取该map集合的所有value
2.2.1 数据的存取
示例代码:
package com.dfbz.demo01;
import java.util.Collection;
import java.util.HashMap;
public class Demo {
public static void main(String[] args) {
//创建 map对象
HashMap<String, String> map = new HashMap();
//添加元素到集合
map.put("江西", "南昌");
map.put("湖南", "长沙");
map.put("湖北", "武汉");
// 获取该map集合的所有value
Collection<String> values = map.values();
System.out.println(values); // [长沙, 武汉, 南昌]
}
public static void test1(){
//创建 map对象
HashMap<String, String> map = new HashMap();
//添加元素到集合
map.put("江西", "南昌");
map.put("湖南", "长沙");
map.put("湖北", "武汉");
// 存取是无序的
System.out.println(map); // {湖南=长沙, 湖北=武汉, 江西=南昌}
// String remove(String key): 根据key来删除记录,并将key对应的value返回
System.out.println(map.remove("江西")); // 南昌
System.out.println(map); // {湖南=长沙, 湖北=武汉}
// 查看 湖北的省会 是哪座城市
System.out.println(map.get("湖北")); // 武汉
System.out.println(map.get("湖南")); // 长沙
}
}
运行结果:
2.2.2 数据的遍历
方法:
public V get(Object key)
根据指定的键,在Map集合中获取对应的值。public Set<K> keySet()
: 获取Map集合中所有的键,存储到Set集合中。
步骤:
- 1)根据
keySet()
方法获取所有key的集合 - 2)通过foreach方法遍历key集合,拿到每一个key
- 3)通过
get()
方法,传递key获取key对应的value;
示例代码:
package com.dfbz.demo01;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo02 {
public static void main(String[] args) {
Map<String, String> cities = new HashMap<>();
//添加元素到集合
cities.put("广西", "南宁");
cities.put("云南", "昆明");
cities.put("贵州", "贵阳");
// 1.获取key的集合
Set<String> provinces = cities.keySet(); // [贵州, 广西, 云南]
System.out.println(provinces);
// 2.遍历key的集合
for (String province : provinces) {
// 3.根据key(省份)拿到value(省会城市)
String city = cities.get(province);
System.out.println(province + "省的省会是:" + city);
}
}
}
运行结果:
2.2.3 Entry对象
Map集合中几条记录存储的是两个对象,一个是key,一个是value,这两个对象加起来是map集合中的一条记录,也叫一个记录项;这个记录项在Java中被Entry对象所描述;一个Entry对象中包含有两个值,一个是key,另一个则是key对应的value,因此一个Map对象我们可以看做是多个Entry对象的集合,即一个Set<Entry>
对象;
Entry是一个接口,是Map接口中的一个内部接口,源码如下:
interface Entry<K,V> {
K getKey();
V getValue();
V setValue(V value);
....
}
HashMap中则提供了Node类对Entry提供了实现,可以看到一个Entry对象(Node对象)中包含有key、value等值:
Map接口中提供有方法获取该Map集合的Entry集合对象:
public Set<Map.Entry<K,V>> entrySet()
: 获取到Map集合中所有的键值对对象的集合(Set集合)。
使用Entry对象来遍历Map集合:
package com.dfbz.demo01;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo03 {
public static void main(String[] args) {
Map<String, String> cities = new HashMap<>();
//添加元素到集合
cities.put("山东", "济南");
cities.put("山西", "太原");
cities.put("河南", "郑州");
cities.put("河北", "石家庄");
// 1.获取该Map的Entry集合
Set<Map.Entry<String, String>> entrySet = cities.entrySet();
// 2.遍历该Entry集合,获取每一个entry,也就是Map中的每一条记录
for (Map.Entry<String, String> entry : entrySet) {
// 获取当前entry对象的key(省份)
String province = entry.getKey();
// 获取当前entry对象的value(城市)
String city = entry.getValue();
System.out.println(province + "省的省会是:" + city);
}
}
}
运行结果:
2.3 HashMap
2.3.1 HashMap简介
HashMap是Map集合中比较常用的实现类,其特点依旧是我们之前学习的HashSet特点;即存储数据采用的哈希表结构(JDK8改为hash表+红黑树),元素的存取顺序不一致。由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
我们之前学习的HashSet底层就是一个HashMap,当我们在讨论HashSet的底层原理时,其实讨论的是HashMap的key的底层原理;
- HashSet底层就是一个HashMap:
查看HashSet的add方法源码:
HashSet的存储的元素都是HashMap的Key,此HashMap的value总是一个固定的Object;
所以,HashSet的去重原理实质上指的就是HashMap的Key的去重原理;
2.3.2 HashMap的去重
我们知道HashSet底层就是依靠HashMap的key去重原理来是实现的,因此Map接口的HashMap、LinkedHashMap等接口的去重都是和HashSet、LinkedHashSet一致;
定义一个City对象:
package com.dfbz.demo01;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class City {
private String name; // 城市名称
private String shortName; // 简称
private String location; // 所属地区
public void intro() {
System.out.println("名称: " + name + ",简称: " + shortName + ",所属地区: " + location);
}
public City() {
}
public City(String name, String shortName, String location) {
this.name = name;
this.shortName = shortName;
this.location = location;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getShortName() {
return shortName;
}
public void setShortName(String shortName) {
this.shortName = shortName;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
}
注意:上面没有重写equals和hashCode方法;
存储两个属性一样的City对象:
package com.dfbz.demo01;
import java.util.HashMap;
import java.util.Map;
public class Deom04 {
public static void main(String[] args) {
Map<City,String> map=new HashMap<>();
map.put(new City("内蒙古","蒙","华北"),"1");
map.put(new City("内蒙古","蒙","华北"),"1");
// 没有重写equals方法底层比较的是地址值,两个City对象肯定不一致,因此存储了2个
System.out.println(map.size()); // 2
}
}
回顾学习HashSet时的去重原理:
存储的两个条件:
-
1)hashCode不同时存储
-
2)当hashCode冲突时,equals为false时存储
没有重写hashCode时两个对象的hashCode一般情况下是不一致的,如果hashCode一致了(hash冲突),equals方法也不可能为true;
重写City的hashCode和equals方法:
按住alt+insert
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
City city = (City) o;
return Objects.equals(name, city.name) &&
Objects.equals(shortName, city.shortName) &&
Objects.equals(location, city.location);
}
@Override
public int hashCode() {
return Objects.hash(name, shortName, location);
}
HashMap基本上没有对Map集合中的方法进行扩展;并且大部分方法在之前学习List接口的时候我们都使用过了,这里就不再演示了;
2.4 LinkedHashMap
1.4.1 LinkedHashMap 特点
我们之前在学习LinkedHashSet时说过,LinkedHashSet是继承与HashSet的,在HashSet底层的基础上增加了一个循环链表,因此LinkedHashSet除了具备HashSet的特点外(唯一),存储的元素还是有序的;
LinkedHashMap继承与HashMap,并且LinkedHashSet底层就是借助于LinkedHashMap来实现的;
LinkedHashSet源码如下:
查看HashSet对应的构造:
查看LinkedHashSet的add方法源码:
HashSet的add方法源码我们之前看过了,实质上是添加到内置的HashMap中去了;
注意:此时的map是LinkedHashMap;
1.4.2 LinkedHashMap 使用
示例代码:
package com.dfbz.demo02;
import java.util.LinkedHashMap;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo01 {
public static void main(String[] args) {
LinkedHashMap<String,String> map=new LinkedHashMap<>();
map.put("安徽","合肥");
map.put("江苏","南京");
map.put("浙江","杭州");
// LinkedHashMap存取是有序的
System.out.println(map); // {安徽=合肥, 江苏=南京, 浙江=杭州}
}
}
运行结果:
2.4 TreeMap
TreeMap也是TreeSet的底层实现,创建TreeSet的同时也创建了一个TreeMap,在往TreeSet集合中做添加操作是,实质也是往TreeMap中添加操作,TreeSet要添加元素成为了TreeMap的key;
我们来回顾一下TreeSet的特点(也是TreeMap的key的特点):
- 必须实现Compareable接口;
- 存储的数据是无序的,但提供排序功能(Comparable接口);
- 存储的元素不再是唯一,具体结果根据compareTo方法来决定;
使用示例1:
package com.dfbz.demo03;
import java.util.Map;
import java.util.TreeMap;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo01 {
public static void main(String[] args) {
Map<Integer,String> map=new TreeMap<>();
map.put(10,"abc");
map.put(5,"abc");
map.put(6,"abc");
map.put(3,"abc");
map.put(1,"abc");
// 根据key排序
System.out.println(map); // {1=abc, 3=abc, 5=abc, 6=abc, 10=abc}
}
}
存储示例2:存储自定义对象,自定义排序规则
定义一个Book类,并重写Compareable方法,按住价格升序排序:
package com.dfbz.demo03;
public class Book implements Comparable<Book> {
private String name;
private Double price;
@Override
public int compareTo(Book book) {
// 根据价格降序排序
return (int) Math.ceil(this.price - book.getPrice());
}
@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public Book() {
}
public Book(String name, Double price) {
this.name = name;
this.price = price;
}
}
测试代码:
package com.dfbz.demo03;
import java.util.Map;
import java.util.TreeMap;
public class Demo02 {
public static void main(String[] args) {
Map<Book,String> map=new TreeMap<>();
map.put(new Book("《计算机网络原理》",48.9),"abc");
map.put(new Book("《高性能MySQL》",68.8),"abc");
map.put(new Book("《深入理解Java虚拟机》",68.9),"abc");
// 根据key排序
// {Book{name='《计算机网络原理》', price=48.9}=abc, Book{name='《高性能MySQL》', price=68.8}=abc, Book{name='《深入理解Java虚拟机》', price=68.9}=abc}
System.out.println(map);
}
}
2.5 Hashtable
Hashtable是原始的java.util的一部分,属于一代集合类, 是一个Dictionary具体的实现 。Java1.2重构的Hashtable实现了Map接口,因此,Hashtable现在集成到了集合框架中。它和HashMap类很相似,但是它支持同步。
2.5.1 Dictionary类
Dictionary类是一代集合中的双列集合顶层类,Dictionary类中的方法都是双列集合中最基本的方法;严格意义来说Java中所有的双列集合都应该继承与Dictionary类,但Java2推出了一系列二代集合,其中二代集合中的Map接口也已经替代了Dictionary接口,成为双列集合的顶层接口,因此Dictionary接口下面没有太多的实现类;
Tips:目前JDK已经不推荐使用Dictionary类了;
- Dictionary接口方法如下:
方法 | 说明 |
---|---|
Enumeration<V> elements() |
返回此字典中值的枚举。 |
V get(Object key) |
返回该字典中键映射到的值。 |
boolean isEmpty() |
检测该字典是否为空。 |
Enumeration<K> keys() |
返回此字典中键的枚举。 |
V put(K key, V value) |
添加一对key,value到字典中 |
V remove(Object key) |
根据对应的key从字典中删除value。 |
int size() |
返回此字典中的条目数。 |
- 方法测试:
package com.dfbz.hashtable;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.Hashtable;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo01_Hashtable基本使用 {
public static void main(String[] args) {
Dictionary<Integer, String> hashtable = new Hashtable<>();
hashtable.put(1, "南昌拌粉");
hashtable.put(2, "粉蒸肉");
hashtable.put(3, "福羹");
hashtable.put(4, "藜蒿炒腊肉");
hashtable.put(5, "瓦罐汤");
String s1 = hashtable.get(3);
System.out.println(s1); // 福羹
String s2 = hashtable.remove(2);
System.out.println(s2); // 粉蒸肉
System.out.println(hashtable); // {5=瓦罐汤, 4=藜蒿炒腊肉, 3=福羹, 1=南昌拌粉}
System.out.println("-------------");
// 获取到Hashtable的所有key
Enumeration<Integer> keys = hashtable.keys();
while (keys.hasMoreElements()){
Integer key = keys.nextElement();
System.out.println(key);
}
System.out.println("-------------");
// 获取到Hashtable的所有value
Enumeration<String> vals = hashtable.elements();
while (vals.hasMoreElements()){
String val = vals.nextElement();
System.out.println(val);
}
System.out.println("-----------------");
System.out.println(hashtable.size()); // 4
}
}
2.5.2 Hashtable与HashMap的区别
-
1)Hashtable属于一代集合,继承了Dictionary类,也实现了Map接口,HashMap属于二代集合,实现与Map接口,没有与Dictionary类产生关系;
-
2)Hashtable支持iterator遍历(Map接口中的),也支持Enumeration遍历(Dictionary),HahsMap只支持iterator遍历
-
2)Hashtable与HashMap底层都是采用hash表这种数据结构,JDK8对HashMap进行了优化(引入红黑树),但并没有对Hashtable进行优化;
-
3)HashMap默认的数组大小是16,Hashtable则是11,两者的负载因子都是0.75,并且都允许传递初始化的数组大小和负载因子
-
4)HashMap对null key和null value进行了特殊处理,可以存储null key和null value,Hashtable则不能存储null key和null value;
-
5)当HashMap存储的元素数量>数组容量*负载因子,数组扩容至原来的2倍,Hashtable则是2倍+1;
-
6)HashMap在添加元素时使用的是:
元素本身的hash算法 ^ (元素本身的hash算法>>> 16)
,而Hashtable则是直接采用元素本身的hash算法;Tips:
>>
代表有符号位移,>>>
代表无符号位移; -
7)HashMap在使用foreach迭代时不能对元素内容进行增删,否则触发并发修改异常。Hahstable中支持Enumeration迭代,使用Enumeration迭代元素时,可以对集合进行增删操作;
-
8)Hashtable是线程安全的,效率低,安全性高;HashMap是线程不安全的,效率高,安全性低;
测试存储Null key和Null value:
package com.dfbz.hashtable;
import java.util.HashMap;
import java.util.Hashtable;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo02_HashMap与Hashtable的区别_null问题 {
public static void main(String[] args) {
HashMap<Integer, String> hashMap = new HashMap<>();
/*
HashMap对null key和null value
并且,HashMap对null key做了特殊处理,HashMap永远将Null key存储在第0位数组上
*/
hashMap.put(1, null);
hashMap.put(null, "大闸蟹");
System.out.println(hashMap); // {null=大闸蟹, 1=null}
}
public static void test1(){
Hashtable<Integer, String> hashtable = new Hashtable<>();
// Hashtable存储null key和null value的时候会出现空指针异常: Exception in thread "main" java.lang.NullPointerException
hashtable.put(1, null);
hashtable.put(null, "大闸蟹");
}
}
- 测试并发修改异常问题:
package com.dfbz.hashtable;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Set;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo03_HashMap与Hashtable的区别_并发修改问题 {
public static void main(String[] args) {
Hashtable<Integer, String> hashtable = new Hashtable<>();
hashtable.put(1, "拌粉");
hashtable.put(2, "汤粉");
hashtable.put(3, "炒粉");
hashtable.put(4, "泡粉");
Enumeration<Integer> keys = hashtable.keys();
while (keys.hasMoreElements()) {
Integer key = keys.nextElement();
if (key == 2) {
/*
Hashtable在使用Enumeration遍历时,允许对集合进行增删操作
注意: Hashtable使用foreach迭代也不能对元素进行增删操作
*/
hashtable.put(5, "扎粉");
// hashtable.remove(3);
}
}
System.out.println(hashtable);
}
/**
* hashMap在使用foreach迭代时不允许对集合进行增删等操作
*/
public static void test1() {
HashMap<Integer, String> hashMap = new HashMap<>();
hashMap.put(1, "拌粉");
hashMap.put(2, "汤粉");
hashMap.put(3, "炒粉");
hashMap.put(4, "泡粉");
Set<Integer> keys = hashMap.keySet();
for (Integer key : keys) {
if (key == 2) {
// hashMap在迭代时不允许对集合进行增删等操作
hashMap.remove(3);
// hashMap.put(5, "扎粉");
}
}
}
}
三、异常
3.1 异常概述
3.1.1 什么是异常
程序运行过程中出现的问题在Java中被称为异常,异常本身也是一个Java类,封装着异常信息;我们可以通过异常信息来快速定位问题所在;我们也可以针对性的定制异常,如用户找不到异常、密码错误异常、页面找不到异常、支付失败异常、文件找不到异常等等…
当程序出现异常时,我们可以提取异常信息,然后进行封装优化等操作,提示用户;
注意:语法错误并不是异常,语法错了编译都不能通过(但Java有提供编译时异常),不会生成字节码文件,根本不能运行;
默认情况下,出现异常时JVM默认的处理方式是中断程序执行,因此我们需要控制异常,当出现异常后进行相应修改,提供其他方案等操作,不要让程序中断执行;
我们之前有见到过很多的异常:
- 空指针异常:
java.lang.NullPointerException
String str=null;
str.toString();
- 数字下标越界异常:
java.lang.ArrayIndexOutOfBoundsException
int[] arr = {1, 3, 4};
System.out.println(arr[3]);
- 类型转换异常:
java.lang.ClassCastException
class A {
}
class B extends A {
}
class C extends A {
}
public static void main(String[] args) {
A a = new B();
C c = (C) a;
}
- 算数异常:
java.lang.ArithmeticException
int i=1/0;
- 日期格式化异常:
java.text.ParseException
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date parse = sdf.parse("2000a10-24");
3.1.2 异常体系
Java程序运行过程中所发生的异常事件可分为两类:
- Error:表示严重错误,一般是JVM系统内部错误、资源耗尽等严重情况,无法通过代码来处理;
- Exception:表示异常,一般是由于编程不当导致的问题,可以通过Java代码来处理,使得程序依旧正常运行;
Tips:我们平常说的异常指的就是Exception;因为Exception可以通过代码来控制,而Error一般是系统内部问题,代码处理不了;
3.1.3 异常分类
异常的分类是根据是在编译器检查异常还是在运行时检查异常;
- 编译时期异常:在编译时期就会检查该异常,如果没有处理异常,则编译失败;
- 运行时期异常:在运行时才出发异常,编译时不检测异常;
Tips:在Java中如果一个类直接继承与Exception,那么这个异常将是编译时异常;如果继承与RuntimeException,那么这个类是运行时异常。即使RuntimeException也继承与Exception;
- 编译时异常举例:
public class Demo {
public static void main(String[] args) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = sdf.parse("2000-10-24");
}
}
- 运行时异常:
public class Demo {
public static void main(String[] args) {
int i = 1 / 0;
}
}
3.2 异常的处理
Java程序的执行过程中如出现异常,会自动生成一个异常类对象,该异常对象将被提交给Java运行时系统(JVM),这个过程称为抛出(throw)异常。
如果一个方法内抛出异常,该异常会被抛到调用方法中。如果异常没有在调用方法中处理,它继续被抛给这个调用方法的调用者。这个过程将一直继续下去,直到异常被处理。这一过程称为捕获(catch)异常。如果一个异常回到main()方法,并且main()也不处理,则程序运行终止。
流程如下:
3.2.1 异常的捕获
异常的捕获和处理需要采用 try 和 catch 来处理,具体格式如下:
- 1)
try...catch(){}
:
try {
// 可能会出现异常的代码
} catch (Exception1 e) {
// 处理异常1
} catch (Exception2 e) {
// 处理异常2
} catch (ExceptionN e) {
// 处理异常N
}
Tips:后处理的异常必须是前面处理异常的父类异常;
- 2)
try...catch(){}...finally{}
:
try {
// 可能会出现异常的代码
} catch (Exception1 e) {
// 处理异常1
} catch (Exception2 e) {
// 处理异常2
} catch (ExceptionN e) {
// 处理异常N
} finally {
// 不管是否出现异常都会执行的代码
}
- 3)
try...finally{}
:
try {
// 可能会出现异常的代码
} finally {
// 不管是否出现异常都会执行的代码
}
示例代码:
package com.dfbz.demo01;
public class Demo01 {
public static void main(String[] args) {
method();
System.out.println("程序终止我就不能执行了~");
}
public static void method() {
try {
String str = null;
System.out.println(str.toString());
} catch (Exception e) {
System.out.println("执行代码出现异常了!");
} finally {
System.out.println("释放一些资源...");
}
}
}
运行结果:
tips:try…catch语句是可以单独使用的;即:不要finally代码块;
需要注意的是:如果finally有return语句,则永远返回finally中的结果。我们在开发过程中应该避免该情况;
- 示例代码:
package com.dfbz.demo02;
public class Demo02 {
public static void main(String[] args) {
int result = method();
System.out.println("method方法的返回值: " + result);
}
public static int method() {
try {
int i = 10;
return 1;
} catch (Exception e) {
e.printStackTrace();
return 2;
} finally {
return 3; // 不管是否出现异常都是返回3
}
}
}
3.2.2 异常的常用方法
在Throwable类中具备如下几个常用异常信息提示方法:
-
public void printStackTrace()
:获取异常的追踪信息;包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。
-
public String getMessage()
:异常的错误信息;
异常触发被抓捕时,异常的错误信息都被封装到了catch代码块中的Exception类中了,我可以通过该对象获取异常错误信息;
示例代码:
package com.dfbz.demo01;
public class Demo02 {
public static void main(String[] args) {
method();
System.out.println("我继续执行~");
}
public static void method() {
try {
int i=1/0;
} catch (Exception e) {
System.out.println("异常的错误信息: " + e.getMessage());
// 打印异常的追踪信息
e.printStackTrace();
}
}
}
运行结果如下:
异常的追踪信息可以帮助我们追踪异常的调用链路,一步一步找出异常所涉及到的方法,在实际开发非常常用;
3.2.3 异常的抛出
我们已经学习过出现异常该怎么抓捕了,有时候异常就当做提示信息一样,在调用者调用某个方法出现异常后及时针对性的进行处理,目前为止异常都是由JVM自行抛出,当然我们可以选择性的自己手动抛出某个异常;
Java提供了一个throw关键字,它用来抛出一个指定的异常对象;抛给上一级;
Tips:自己抛出的异常和JVM抛出的异常是一样的效果,都要进行处理,如果是自身抛出的异常一直未处理,最终抛给JVM时程序一样会终止执行;
语法格式:
throw new 异常类名(参数);
示例:
throw new NullPointerException("调用方法的对象是空的!");
throw new ArrayIndexOutOfBoundsException("该索引在数组中不存在,已超出范围");
示例代码:
package com.dfbz.demo01;
public class Demo03 {
public static void main(String[] args) {
method(null);
System.out.println("我还会执行吗?");
}
public static void method(Object object) {
if (object == null) {
// 手动抛出异常(抛出异常后,后面的代码将不会被执行)
throw new NullPointerException("这个对象是空的!不能调用方法!");
}
System.out.println(object.toString());
}
}
运行结果:
手动抛出的异常和JVM抛出的异常是一个效果,也需要我来处理抛出的异常;
修改代码:
package com.dfbz.demo01;
public class Demo03 {
public static void main(String[] args) {
try {
method(null);
} catch (Exception e) {
System.out.println("调用method方法出现异常了");
}
System.out.println("我还会执行吗?");
}
public static void method(Object object) {
if (object == null) {
// 手动抛出异常(抛出异常后,后面的代码将不会被执行)
throw new NullPointerException("这个对象是空的!不能调用方法!");
}
System.out.println("如果出现了异常我是不会执行了,你能执行到这里说明没有异常");
System.out.println(object.toString());
}
}
运行结果:
3.2.4 声明异常
1)运行时异常
在定义方法时,可以在方法上声明异常,用于提示调用者;
Java提供throws关键字来声明异常;关键字throws运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常);
语法格式:
... 方法名(参数) throws 异常类名1,异常类名2…{ }
代码示例:
package com.dfbz.demo01;
import java.text.ParseException;
public class Demo04 {
public static void main(String[] args) {
// 可以处理,也可以不处理
method("你好~");
// 编译时异常必须处理
try {
method2("hello~");
} catch (ParseException e) {
System.out.println("出现异常啦!");
}
}
// 调用此方法可能会出现空指针异常,提示调用者处理异常
public static void method(Object obj) throws NullPointerException {
System.out.println(obj.toString());
}
// 抛出的不是运行时异常,调用者调用该方法时必须处理
public static void method2(Object obj) throws ParseException {
System.out.println(obj.toString());
}
// 也可以同时抛出多个异常
public static void method3(Object obj) throws ClassCastException,ArithmeticException {
System.out.println(obj.toString());
}
}
2)编译时异常
在声明和抛出异常时需要注意如下几点:
- 1)如果是抛出(throw)编译是异常,那么必须要处理,可以选择在方法上声明,或者try…catch处理
- 2)如果调用的方法上声明(throws)了编译时异常,那么在调用方法时就一定要处理这个异常,可以选择继续网上抛,也可以选择try…catch处理
- 示例代码:
package com.dfbz.demo04;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo02 {
public static void main(String[] args) throws Exception{
// 如果调用的方法上声明了编译时异常,那么在调用方法时就一定要处理这个异常,可以选择继续网上抛,也可以选择try..catch处理
test(500);
}
// 如果方法声明了编译时异常,那么在调用方法时就一定要处理这个异常
public static void test(Integer num) throws MyException{
if (num == 100) {
// 如果是抛出编译是异常,那么必须要处理,可以选择在方法上声明,或者try...catch处理
throw new MyException();
}
}
}
// 直接继承与Exception,那么这个异常是一个编译时异常
class MyException extends Exception {
}
3.3 自定义异常
我们说了Java中不同的异常类,分别表示着某一种具体的异常情况,那么在开发中总是有些异常情况是Java中没有定义好的,此时我们根据自己业务的异常情况来定义异常类。
我们前面提到过异常分类编译时异常和运行时异常:
-
1)继承于
java.lang.Exception
的类为编译时异常,编译时必须处理; -
2)继承于
java.lang.RuntimeException
的类为运行时异常,编译时可不处理; -
自定义用户名不存在异常:
package com.dfbz.demo04;
public class UsernameNotFoundException extends RuntimeException {
/**
* 空参构造
*/
public UsernameNotFoundException() {
}
/**
* @param message 表示异常提示
*/
public UsernameNotFoundException(String message) {
// 调用父类的构造方法
super(message);
}
}
- 测试类:
package com.dfbz.demo04;
public class Demo01 {
// 定义内置账户
public static String[] users = {"xiaohui", "xiaolan", "xiaoliu"};
public static void main(String[] args) {
findByUsername("abc");
}
public static void findByUsername(String username) throws UsernameNotFoundException {
for (String user : users) {
if (username.equals(user)) {
System.out.println("找到了: " + user);
return;
}
}
// 用户名没找到
throw new UsernameNotFoundException("没有用户: " + username);
}
}
运行结果:
3.4 方法的重写与异常
- 1)子类在重写方法时,父类方法没有声明编译时异常,则子类方法也不能声明编译时异常;
需要注意的是:运行时异常没有这个规定;也就是子类在重写父类方法时,不管父类方法是否有声明异常,子类方法都可以声明异常;
package com.dfbz.demo05;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo01 {
public static void main(String[] args) throws Exception {
}
}
// 定义一个编译时异常
class MyException extends Exception {
}
class Fu {
public void method() {
}
public void method2() {
}
}
class Zi extends Fu {
// 语法通过,运行时异常随意
public void method() throws NullPointerException {
}
// 语法报错,父类方法没有声明编译时异常,那么子类重写方法时也不能声明编译时异常
public void method2() throws MyException {
}
}
- 2)同样是在编译时异常中,在子类重写父类方法时,子类不可以声明比父类方法大的异常;
package com.dfbz.demo06;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo01 {
public static void main(String[] args) throws Exception {
}
}
// 定义一个编译时异常
class MyException extends Exception {
}
class Fu {
public void method() throws NullPointerException{
}
public void method2()throws MyException{
}
}
class Zi extends Fu {
// 运行时异常没有问题
public void method() throws RuntimeException {
}
// 语法报错,如果是编译时异常,子类在重写父类方法时,不可以抛出比父类大的编译时异常
public void method2() throws Exception {
}
}
记得点赞~!!!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/131774.html