6

Java基础笔记(二) 集合、泛型、异常处理

 2 years ago
source link: https://songlee24.github.io/2016/03/16/java-basic-note-2/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文主要是我在看《疯狂Java讲义》时的读书笔记,阅读的比较仓促,就用 markdown 写了个概要。

第七章 Java集合(重点)

1、 Java集合概述

Java集合类是一种工具类,主要用来存储数量不定的对象,类似于容器。Java主要有四大集合体系:SetListQueueMap

所有的集合类都位于java.util包下,后来为了支持多线程又在java.util.concurrent包下提供了一些线程安全的集合类(本章不讨论)。

Java集合类主要派生自两个接口类:CollectionMap,继承树如下图:





图中粗边框的Set、List、Queue、Map仍然是接口,而以灰色覆盖的是常用的实现类,比如HashSetTreeSetArrayListLinkedListArrayDequeHashMapTreeMap等。

2、如何遍历集合

Iterator接口遍历集合

Iterator迭代器提供了遍历Collection集合元素的统一编程接口,它定义了几个方法:

  • boolean hasNext():集合元素是否被遍历完
  • Object next():返回集合里的下一个元素
  • void remove():删除集合里上一次next方法返回的元素
  • void forEachRemaining(Consumer action):Java 8新增方法,用于Lambda表达式遍历集合。
import java.util.HashSet;
import java.util.Iterator;

public class MyClass {
public static void main(String[] args) {
HashSet<String> set = new HashSet<String>();
set.add("Firstly");
set.add("Secondly");
set.add("Thirdly");

Iterator it = set.iterator();
while(it.hasNext()) {
System.out.println(it.next());
}
}
}

Lambda表达式遍历集合

Java 8支持lambda表达式,并且为每个可迭代的集合类新增了一个forEach(Consumer action)方法,参数类型是一个函数式接口。

import java.util.HashSet;

public class MyClass {
public static void main(String[] args) {
HashSet<String> set = new HashSet<String>();
set.add("Firstly");
set.add("Secondly");
set.add("Thirdly");
set.forEach(obj -> System.out.println(obj)); // Lambda表达式
}
}



foreach循环遍历集合

类似于C++11中的范围循环

import java.util.HashSet;

public class MyClass {
public static void main(String[] args) {
HashSet<String> set = new HashSet<String>();
set.add("Firstly");
set.add("Secondly");
set.add("Thirdly");
for(Object obj : set) {
System.out.println(obj);
}
}
}



3、Set集合

HashSet类

  • 采用Hash算法来存储集合中的元素(由hashCode值决定存储位置),故拥有很好的存取和查找性能。
  • 不允许包含相同的元素,相同的标准是“通过equals()比较相等且hashCode()返回值也相等”
  • 不能保证元素的排列顺序,顺序可能与添加顺序不同。
  • 集合元素值可以是null

TreeSet类

  • 采用红黑树来存储集合中的元素。
  • 不允许包含相同的元素,相同的标准是“通过compareTo(Object obj)比较返回0”
  • 集合中的元素是有序的,默认使用compareTo升序排列,当然你也可以通过Comparator对象自定义排序规则。
  • 集合元素不可以是null

EnumSet类

  • 采用位向量的形式存储元素,紧凑高校,运行效率高。
  • 集合中的多个枚举值必须属于同一个枚举类
  • 各元素按Enum类内的定义顺序有序
  • 集合元素不可以是null

结论:上述Set的三个实现类都不是线性安全的。HashSetTreeSet作为Set类的两个典型实现,前者的性能总是比后者好,因为后者需要额外维护一棵红黑树,但后者是有序的,所以需要根据具体需求来选择。


4、Queue集合

Queue用于模拟“先进先出”队列,不允许随机访问。

PriorityQueue类

  • 不同于FIFO队列,PriorityQueue是按优先权排列,默认就是按元素大小进行排列。
  • 本质上是一个最小堆
  • 不允许插入null元素

Deque接口与ArrayDeque类

  • Deque是Queue的子接口,它代表一个双端队列。而ArrayDeque类是Deque的一个典型实现。
  • ArrayDeque类是基于数组实现的。
  • ArrayDeque类是一个双端队列,所以即可以作为队列使用,也可以作为栈使用。

5、List集合

ArrayList类与Vector类

  • ArrayList类和Vector类都是基于数组实现的(Object[]);
  • ArrayList类和Vector类在用法上完全相同,但Vector是一个古老的类,有很多缺点,尽量少用;
  • ArrayList类和Vector类的显著区别:ArrayList是线程不安全的,Vector是线程安全的。

LinkedList类

  • 同时实现了List接口与Deque接口,所以可以作为List集合、双端队列、栈使用。
  • LinkedList类是基于链表实现的,插入/删除性能好。

6、Map集合

HashMap类与Hashtable类

  • HashMap与Hashtable的关系完全类似于ArrayList和Vector,Hashtable太古老,尽量少用;
    • HashMap线程不安全,Hashtable线程安全
    • HashMap可以插入null作为key/value,但Hashtable不可以
  • HashMap/Hashtable不能保证元素的顺序,因为它们的key保存方式与HashSet完全相同。
  • HashMap/Hashtable判断两个key相等的标准:两个key通过equals()比较返回true时,它们的hashCode()也相等。

TreeMap类

  • 基于红黑树实现,每个kv对就是红黑树的一个节点。
  • 类似于TreeSet类,TreeMap类根据key保持有序。默认使用 compareTo() 升序排列,当然你也可以通过 Comparator 对象自定义排序规则。
  • 不允许包含相同的元素,相同的标准是“通过compareTo(Object obj)比较返回0”

EnumMap类

  • 类似于EnumSet类,EnumMap类的key必须是同一个枚举类的枚举值
  • 根据 key 有序(枚举类中定义的顺序)
  • 不允许null作为key,但允许null作为value。

7、什么是rehash?

所谓的rehash,是指当hash表中的槽位被填满到一定程度(最大负载因子)时,hash表会自动成倍地增加容量,并将原来的对象重新分配,hash到新的表中。

最大负载因子,即负载极限。HashSet、HashMap、Hashtable的默认负载极限是0.75,这是时间和空间上的一种折中,在保证查询性能的同时,尽量减少哈希表的内存开销。


8、操作集合的工具类Collections

Java提供了一个操作Set、List和Map等集合的工具类Collections,该工具类里提供了大量方法对集合元素进行排序、查找和修改……另外,还可以对集合对象实现同步控制以及将集合对象设置为不可变。

线程同步控制

上面介绍的集合类中,除了古老的Vector和Hashtable之外,都是线程不安全的。Collections工具类提供了多个synchronizedXxx()方法,用于将指定集合包装成线程安全的集合:

import java.util.*;

public class MyClass {
public static void main(String[] args) {
List l = Collections.synchronizedList(new ArrayList());
Set s = Collections.synchronizedSet(new HashSet());
Map m = Collections.synchronizedMap(new HashMap());
}
}

设置不可变集合

Collections提供了三个方法来返回一个不可变的集合:

  • emptyXxx():返回一个空的、不可变的集合对象。
  • singletonXxx():返回一个只有一个元素、且不可改变的集合对象。
  • unmodifiableXxx():返回指定集合对象的不可变版本(只读)。

第八章 泛型

1、泛型的概念

在Java没有泛型之前,一旦把一个对象“丢进”Java集合中,集合就会忘记对象的类型,把所有对象当成 Object 类型处理。当从集合中取出对象后,就需要进行强制类型转换。这种强制类型转换不仅使代码臃肿,而且容易引起ClassCastException异常。下面就是一个例子:

import java.util.ArrayList;

public class MyClass {
public static void main(String[] args) {
ArrayList strs = new ArrayList();
strs.add("one");
strs.add("two");
strs.add(3); // 把一个Integer对象丢进了集合
// 遍历输出时报ClassCastException异常
strs.forEach(str -> System.out.println(((String)str).length()));
}
}

从Java 5以后,Java引入了“参数化类型”的概念,允许程序在创建集合时指定集合元素的类型。注意,如果没有传入类型实参,那么类型参数 T 将会当成Object类型处理。

ArrayList<String> strs = new ArrayList<String>();
ArrayList<String> strs = new ArrayList<>(); // 后面可以只带尖括号

这种参数化类型就称为泛型(Generic)


2、泛型的好处

  • 增加泛型支持之后,集合完全可以记住元素的类型,并可以在编译时检查添加元素是否满足类型要求,不满足的话,编译器会提示错误;
  • 泛型可以减少强制类型转换,使代码更加简洁;
  • 泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常,使程序更加健壮。

3、自定义泛型类

相信学过《C++函数模板与类模板》的,对Java的泛型编程并不难理解,这里就不赘述了。示例:

public class People<T> {

private T info;

public People(){}
public People(T info) {
this.info = info;
}

public T getInfo() {
return this.info;
}

public static void main(String[] args) {
People<String> he = new People<>("James Bond");
People<Integer> she = new People<>(25);
System.out.println("his name is "+he.getInfo()+", her age is "+she.getInfo());
}
}

另外,Java中还可以对类型参数 T 进行限制:

public class People<T extends Number> {
//......
}

这时传入的类型实参 必须是Number类或它的子类。


4、类型通配符

将一个问号作为 类型实参 传给支持泛型的集合,比如List<?>,它的元素类型可以匹配任何类型,这个问号被称为通配符

import java.util.*;

public class MyClass{
// 遍历元素类型未知的List集合
public void traverse(List<?> list) {
list.forEach(e -> System.out.println(e));
}

public static void main(String[] args) {
List<String> myList = new ArrayList<>();
myList.add("Kobe");
myList.add("LeBron");
MyClass myClass = new MyClass();
myClass.traverse(myList);
}
}

设定通配符的上限

List<?>表示匹配所有的类型。但是有时候我们希望只匹配一部分,比如只匹配某个类以及它的子类,就可以这样写:

List<? extends People>

这里的问号?仍代表一个未知类型,但这个未知类型必须是People类或它的子类。

设定通配符的下限

List<? super People>

这里的问号?仍代表一个未知类型,但这个未知类型必须是People类或它的父类。


5、自定义泛型方法

语法如下:

修饰符 <T, S> 返回值类型 函数名(形参列表)
{
// 函数体
}

例如下面这个泛型方法:

static <T> void myFunction(Collection<T> arr, Collection<T> c) {
for(T elem : arr) {
c.add(elem);
}
}

上述泛型方法可以直接调用,编译器会根据实参推断类型参数的值。但其实,也可以直接用类型通配符替代(两个Collection的元素类型没有依赖关系的情况下):

static void myFunction(Collection<?> arr, Collection<?> c) {
for(T elem : arr) {
c.add(elem);
}
}

第九章 异常处理

1、Java异常的继承体系

如上图所示,java.lang.Throwable是Java中所有可以错误和异常的父类。Java把所有的非正常情况分为两类:

  • Error(错误):一般指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,应用程序无法处理这些错误。

  • Exception(异常):指应用程序本身可以处理的异常。

    • 运行时异常:指RuntimeException类及其子类异常,编译器不会检查这些异常。
    • 非运行时异常:也叫Checked异常,是指RuntimeException以外的 Exception。这些异常不处理,程序就不能编译通过。如IOExceptionSQLException

2、异常处理机制

Java的异常机制主要依赖于trycatchfinallythrowthrows五个关键字。

捕获异常:try-catch语句

try {
statement1
statement2 // 出现异常,系统生成异常对象ex
......
} catch (ExceptionClass1 e1) { // ex对象是否属于ExceptionClass1类及其子类
exception handler statement1
......
} catch (ExceptionClass2 e2) { // 一旦捕获,try-catch语句结束
exception handler statement
......
}
......

如果找不到能捕获该异常的catch块,则运行时环境终止,Java程序也将退出。

捕获异常:try-catch-finally语句

try-catch 语句还可以包括第三部分,就是finally块。无论是否出现异常,finally块总会被执行;甚至在try块或catch块中return了,finally块也会被执行。(除非System.exit()

try {
// 业务实现代码
......
} catch (ExceptionClass1 e1) {
// 异常处理块1
......
} catch (ExceptionClass2 e2) {
// 异常处理块2
......
}
......
finally {
// 资源回收块
......
}

finally块通常用来回收在try块里打开的一些物理资源,例如数据库连接、网络连接、磁盘文件等。

抛出异常:throws

throws关键字只能用在方法签名中。如果当前函数不知道如何处理这种异常,则应该使用 throws 抛出异常,交由上一级调用者处理 —— 若main方法 throws 异常,该异常将交给JVM处理(打印跟踪栈信息并终止程序运行)。

public static void main(String[] args) throws IOException

抛出异常:throw

当程序出现错误时,系统会自动抛出异常;除此之外,程序也可以自己抛出异常。throw单独作为语句使用,用于抛出一个具体的异常对象。

throw new Exception("您输入的格式有误!");



3、打印异常信息

当在catch块中捕获了异常,你可能想要获取或打印异常对象的相关信息。所有的异常对象都包含几个常用方法:

  • getMessage():返回该异常的详细描述字符串。
  • printStackTrace():将该异常的跟踪栈信息输出到标准错误输出。
  • printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流。
  • getStackTrace():返回该异常的跟踪栈信息。

4、自定义异常类

用户自定义异常都应该继承Exception基类,当然如果希望自定义运行时异常,则应该继承RuntimeException基类。

定义异常类时通常需要提供两个构造器:一个是无参数的构造器,另一个是带一个字符串参数的构造器(该字符串作为异常对象的描述信息)。

public class InputException extends Exception 
{
public InputException(){}
public InputException(String msg) {
super(msg);
}
}



5、异常链

对于大型应用而言,通常有严格的分层关系,上层功能的实现依赖下层的API,如图:

如果当 中间层 访问 持久层出现SQLException异常时,程序不应该把底层的SQLException异常传到用户界面,因为:

  • 用户并不想看到底层的SQLException异常,该异常对他们使用该系统没有任何帮助;
  • 将底层异常暴露出来不安全。

所以通常的做法就是:程序先捕获原始异常,然后抛出一个新的业务异常。(新的业务异常中包含对用户的提示信息)

try {
// 业务逻辑代码
......
} catch (SQLException sqle) {
// 把原始异常记录下来,留给管理员
......
throw new UserException("访问数据库出现异常");
} catch (Exception e) {
//把原始异常记录下来,留给管理员
......
throw new UserException("系统出现未知异常");
}

这种捕获一个异常然后接着抛出另一个异常,并把原始异常信息保存下来是一种典型的链式处理,也被称作“异常链”

从Java 4以后,所有Throwable子类在构造器中都可以接收一个Exception对象,这样就可以很容易把原始异常作为参数传递给新的异常,创建并抛出新的异常。也能通过异常链追踪到异常最初发生的位置。示例如下:

public class UserException extends Exception 
{
public UserException(){}
public UserException(String msg) {
super(msg);
}
public UserException(Throwable t) { // 带Throwable参数的构造器
super(t);
}
}
/*---------------------------------------------*/
try {
// 业务逻辑代码
...
} catch (SQLException sqle) {
throw new UserException(sqle); // 封装原始异常
} catch (Exception e) {
throw new UserException(e); // 封装原始异常
}



About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK