Java基础篇(二)

本文介绍Java常用集合类,包括List、Set、Map接口及其实现类特点、使用场景和优化方法,还讲解集合中引用存储及浅复制和深复制。此外,阐述异常处理,如try…catch…finally语句、throw和throws区别及异常处理使用要点,以保障程序稳定运行。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java常用集合类

根据数据存储格式的不同,Java集合可以分为两类,一类是以Collection为基类的线性表类(list和set子接口),另一类是以Map为基类的键值对类,集合是容器,其中可以存储基本数据类型和自定义Class类型数据。

List接口

List集合是线性数据结构的主要实现,在集合中保存的都是引用类型(如果是基本数据类型的话,会自动转成包装类),List是有序的,可重复的,有索引的集合,List接口的实现类有ArrayList、Linked List、Vector,下面我们来具体分析一下这些实现类。

ArrayList

ArrayList继承于AbstractList抽象类,是容量可以改变的非线程安全集合,内部实现使用数组进行存储,在扩容时会创建更大的数组空间,把原有的数据复制到新数组中,初始化默认长度为10,超出后以0.5倍延长,性能消耗主要体现在数据的移动和复制上。
ArrayList特点为查询快、增删慢、线程不够安全、效率高。

LinkedList

LinkedList本质是双向链表,除了继承AbstractList外还实现了Deque接口,同时具有队列和栈的性质(队列:先进先出;栈:后进先出),其性能消耗在于如果想查找某一元素的话,只能从头依次查找,但是添加或删除元素的时候,只需要改变指针即可,不需要移动元素。
LinkedList的特点为查询慢、增删快、线程不安全、效率高且内存利用率高(通过链表可以将零散的内存单元关联起来,形成按链路顺序查找的线性结构)。

Vector

Vector底层同样是数组结构,默认长度为10,超出后以1倍延长,可能会浪费空间。
Vector的特点是查询快、增删慢、线程安全、效率不够高,几乎已经不用。

Set接口

set是不允许出现重复元素的集合类型,其实现类有HashSet、LinkedHashset、TreeSet。

Hash Set

Hash Set继承于AbstractSet抽象类,从源码分析是用HashMap实现的,将value固定为一个静态对象,使用key保证集合元素的唯一性,但不保证元素的添加顺序,可以添加null元素。

HashSet存储过程分析

当向HashSet添加元素时,会先调用该对象的hashCode()方法,得到该对象的hashCode值,然后根据hashCode值决定该对象在HashSet中的存储位置。
如果hashCode不同,直接把元素存储到hashCode指定的位置上;
如果hashCode相同,则继续通过equals方法判断该元素和集合中的对象是否为同一对象,如果equals为true,则视为同一个对象,不保存;如果equals为false,则将元素存储在之前对象同槽位的链表上,这非常麻烦,应该避免这种情况的发生,即应该保证equals为true时,这两个对象的hashCode值也应该相同,所以对象所在类必须重写hashCode和equals方法(基本数据类型和String类型已经重写过了hashCode和equals方法)。

LinkedHashSet

Linked Hash Set继承自HashSet,具有HashSet的优点,内部使用链表维护了元素的插入顺序,存取一致,即通过链表保证元素添加的顺序,通过哈希保证元素的唯一性。

TreeSet

TreeSet从源码分析是用TreeMap实现的,底层为树结构,在添加元素时会自动排序但不保证元素添加的顺序,要求放入TreeSet中对象所在类必须实现Comparable接口,重写CompareTo和equals方法,保证元素插入集合中仍然有序,且元素类型必须一致,不然会报错。

Map接口

Map集合是以key-value键值对作为存储元素实现的哈希结构,存储无序,可以使用keySet()获取所有的key,使用values()获取所有的value,Map.entrySet()查看所有的key-value。
Map的实现类有HashMap、Linked Hash Map、Tree Map、HashTable、ConcurrentHashMap等。

HashMap

最常用的map,可以存放null值,线程不安全,访问速度快。

LinkedHashMap

HashMap的子类,可以存放null值,保证记录的插入顺序,线程不安全。

TreeMap

保存时根据key排序,不可以存放null值,线程不安全。

HashTable

不可以存放null值,线程安全,性能不高,几乎被淘汰

ConcurrenHashMap

不可以存放null值,在多线程并发场景中,优先推荐使用。

集合中存放的是引用:通过浅复制和深复制理解

我们自定义的类是以引用的形式放入集合中的,如使用不当会引发隐蔽的错误,举个栗子,
定义个类,其中有个id属性,然后创建一个实例,并初始化id为1,然后将这个实例分别放入两个list中,然后在其中一个list中更改这个实例的id为2,另一个不做修改,那么另一个没做修改中的实例的id属性是1还是2?通过代码演示一下,

public class StringTest {

	public static void main(String[] args) {
		TestClass tc=new TestClass();
		tc.id=1;
		ArrayList <TestClass>list1=new ArrayList<TestClass>(); 
		ArrayList <TestClass>list2=new ArrayList<TestClass>(); 
		list1.add(tc);
		list2.add(tc);
		list1.get(0).setId(2);
		System.out.println(list2.get(0).getId());
		
	}

}
class TestClass{
	int id;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}
}

打印结果为2,其原因就是集合中存放的是引用,结合图来说明一下,
在这里插入图片描述
如图所示,我们对list1中的tc做更改时,其实是通过tc引用改变了内存中的id,由于list2存放的也是tc引用,虽然没有更改list2中的存放的tc对象,但list2中的值也跟着变了。
通常我们将同一个对象放到两个不同的集合中时,本意是想为该对象做一个备份,但是上面的做法与我们的预期结果不一样,如果要正确的实现上面描述的备份效果,就需要通过clone方法来实现深复制,代码如下,

public class StringTest {

	public static void main(String[] args) {
		TestClass tc=new TestClass();
		tc.id=1;
		TestClass tc2=null;
		try {
			tc2=(TestClass) tc.clone();
		} catch (CloneNotSupportedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		ArrayList <TestClass>list1=new ArrayList<TestClass>(); 
		ArrayList <TestClass>list2=new ArrayList<TestClass>(); 
		list1.add(tc);
		list2.add(tc2);
		list1.get(0).setId(2);
		System.out.println(list2.get(0).getId());
		
	}

}
class TestClass implements Cloneable{
	int id;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}
	@Override
	protected Object clone() throws CloneNotSupportedException {
		// TODO Auto-generated method stub
		return super.clone();
	}
}

输出结果仍为1。
为了实现clone,我们必须将自定义的类实现Cloneable接口,并重写其中的clone方法,来完成对象的复制,当tc被clone后,系统会开辟一块新的空间存放和tc相同的内容,然后赋值给tc2,这时再用两个list分别存放tc和tc2,就是存放了两个不同的引用,所以更改list1中的tc就不会影响list2中tc2.

各集合使用场景及优化

1、首先根据需求确定集合的类型,如果是单列的考虑使用Collection下的子接口List和Set,如果是映射关系就用Map。
2、确定好集合类型后,再确定使用该集合下的哪个子类,如果迭代时需要有序,就找Linked双向列表结构的集合;如果需要对元素排序就找Tree类型的集合。
3、预计估算集合的数据量,无论是List还是Map,它们实现动态增长都有一定的性能消耗,可以在初始化集合时,给出一个合理的容量,这样会减少动态增长的消耗。
4、使用泛型,可以避免出现异常。
5、尽可能使用集合工具类,它有更好的稳定性和维护性。

异常处理

在程序中,错误(Error)和异常(Exception)会影响正常的运行流程,在java中分别有Error类和Exception类,它们都是Throwable的子类。
对于Error来说,仅靠程序代码本身无法有效地恢复,所以我们一般不做任何处理直接终止程序,不需要过多的关注它的语法。
而Exception是在程序中需要关心的异常类,它会派生一些子类分别处理不同情况抛出的异常。

try…catch…finally语句

try代码块:监听代码执行过程,一旦发现异常直接跳转至catch,如果没有catch则直接跳到 finally。
catch代码块:如果没有异常发生则不会执行;如果发生异常则进行处理或向上抛出。
finally代码块:必选执行代码块,不管是否有异常发生,都会执行。

finally代码块没有被执行的几种可能:
1、没有进入try代码块
2、进入try代码块,但是执行了System.exit()
3、进入try代码块,但是代码运行中出现了死循环或死锁状态

finally执行的特点:
1、finally是在return表达式运行后执行的,此时将return的结果暂存起来,待finally执行结束后,再将之前暂存的结果返回,如,

	    int temp=1000;
		try {
			return ++temp;
		}finally {
			temp=999;
		}

最终返回temp=1001;

2、在finally中使用return,很危险,应避免,比如,

public class StringTest {

	static int x=1;
	public static void main(String[] args) {
		System.out.println(test());
	}
	static int test() {
		try {
			return ++x;
		}finally{
			return ++x;
		}
	}
}

最终返x=3,因为最终的返回动作有finally完成的。

3、锁应加在try代码块之外,避免对未加锁对象解锁,如,

		try {
			lock.lock();
		}finally {
			lock.unlock();
		}

这样写存在隐患,因为进入try代码块中,无论加锁是否成功,都会执行finally的解锁方法,造成了对未加锁对象解锁。

throw ,throws的区别

通过一段代码说明,throw和throws的用法,

public class StringTest {
	public void test()throws Exception{
		throw new Exception();
	}
	public static void main(String[] args) {
		StringTest stringTest=new StringTest();
		try {
			stringTest.test();
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

因为test方法内部通过throw抛出了异常,所以在声明该函数时,一定要配套使用throws,不然会报错,如果throw抛出的是RuntimeException,则不需要使用throws声明。
由于main方法中调用了抛出异常的方法,所以在调用test方法时要用try catch包起来,不然也会报错。
下面总结一下throw和throws使用要点:
1、throw在函数体中使用,而throws出现在声明方法的位置。
2、如果方法内部通过throw抛出了异常,则在声明该方法时,一定要配套使用throws,否则会报错,RuntimeException除外。
3、如果调用一个抛出异常的方法,则在调用该方法的地方要用try catch包起来。

异常处理部分使用要点

1、尽量使用try catch finally处理异常,在finally中应当尽可能的回收内存资源。
2、尽量减少用try监控的代码块,无需被监控的代码,不应放到try 中。
3、先用专业的异常来处理,最后在用Exception异常来处理,如,

		try {
			io代码
			数据库连接代码
		} 
		catch(IOException ioe) {
			处理io异常的代码
		}
		catch(SQLException ioe) {
			处理数据库操作异常的代码
		}
		catch (Exception e) {
			最后用Exception异常
		}

4、出现异常后,应尽量保证项目不会终止,把异常的影响降到最低。
例如,有两个平行的业务,即使其中一个业务出现异常,另一个业务也应该正常执行,错误的写法,

		try {
			业务1
			业务2
		} 
		catch (Exception e) {
			处理异常
		}

应当写成

		try {
			业务1
		} 
		catch (Exception e) {
			处理异常
		}
		try {
			业务2
		} 
		catch (Exception e) {
			处理异常
		}

再有,例如从文件中读100条数据,然后依次写入数据库,即使其中一条插入出错,也不能影响其他插入动作,
先看错误的写法,

	try {
			for(int i=0;i<=100;i++) {
				读其中的一条数据,并插入数据库
			}
		} 
		catch (Exception e) {
			处理异常
		}

这样写的话,如果第一条就出现异常,那么就直接跳到catch中,这样其他数据也无法插入了,那么我们再看正确的写法,

		for(int i=0;i<=100;i++) {
				try {
				读其中的一条数据,并插入数据库
				} 
				catch (Exception e) {
					处理异常;
					continue;
				}
		}

这样的话,即使某条插入失败,也不会影响后续的插入动作,从而把影响降到了最低。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值