JavaImprove--Lesson07--异常处理的两种方式,collection集合

一.Java异常处理的两种方式

Java的异常机制是一种处理程序中错误的方式,它包括异常的抛出、捕获和处理。异常是在程序运行过程中发生的异常事件,如数学运算中的除0异常、数组越界、空指针异常等。

在Java中,异常被视为一种对象,可以通过使用try-catch语句块来捕获和处理。当try块中的代码发生异常时,控制流将立即转到相应的catch块处理异常。如果没有对异常进行处理,则程序运行过程中会报错,程序也无法继续执行。

Java的异常机制还包括对异常类型的定义和管理。Java提供了一些内置的异常类,如IOException、NullPointerException等,这些类继承自Exception类。程序员也可以自定义异常类,以适应特定程序的需要。

在Java中,异常处理是一个重要的概念,可以帮助程序员更好地组织和控制程序的执行流程,确保程序的健壮性和稳定性。

在处理异常中一共有两个方式,

一种是从底层一层一层向上将异常抛出,然后交给最终的调用者处理

一种是直接在最外层捕获异常,也就是直接调用者来处理

一层一层的抛出

一层一层的抛出指的是在进行代码描绘的时候,是一个方法调用另外一个方法,当调用的底层方法有异常的时候,自己先不处理,而是交给上层调用者处理

一般这个最终调用者都是主方法(main)

值得注意的是,如果在main方法中你仍然选择抛出给上层处理,那么最终处理的就是JVM虚拟机,JVM处理的方式使用的就是try -- catch-- 来捕获异常的

示例:

public class ExceptionDemo {
    public static void main(String[] args) {
        //最外层调用者处理异常
        try {
            getSimpleDate();
        } catch (ParseException e) {
            throw new RuntimeException(e);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
    //向调用main方法抛出异常,ParseException,FileNotFoundException
    public static void getSimpleDate() throws ParseException, FileNotFoundException {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date parse = dateFormat.parse("2024-1-14 11:07");
        System.out.println(parse);
        getPng();
    }
    //向调用者getSimpleDate()方法抛出一个:找不到文件异常
    public  static void getPng() throws FileNotFoundException {
        FileInputStream stream = new FileInputStream("D:\\test.png");
    }
}

如上代码:

就是使用的一层一层的抛出,在子方法就是抛出异常,而主方法就捕获异常然后处理这个异常

虽然主方法也可以抛出到JVM,但是实际开发并不建议这么做,还是推荐在统一抛出到main方法,然后由main方法集中处理

最外层捕获

这一类的处理方式并不是直接异常导致的,而是底层方法的失误,导致上层调用者出现的异常

如:底层代码要求输入一个数字类型的数据,然而输入数字是可以正常运行的,而用户输入的是字符时就会报错,因为底层实现者并没有考虑到这块

所以就需要我们调用者来把这个运行时异常捕获并处理掉

示例:

public class ExceptionDemo2 {
    public static void main(String[] args) {
        //最外层调用者处理异常
        try {
            getNumber(); //Exception in thread "main" java.util.InputMismatchException
        } catch (Exception e) {
            System.out.println(e);
        }
    }
    //向调用main方法抛出异常
    public static void getNumber() {
        Scanner scanner = new Scanner(System.in);
        //正常输入数字是没问题的,但是输入的是字符就会报错
        int number = scanner.nextInt();
        System.out.println(number);
    }
}

如上:

这种方式处理的是,底层未考虑到或者底层未处理的异常的,由于底层没有抛出异常,所以上层调用者是不可预知的,只有在开发中遇到了就处理掉就行了

 二.单列集合

集合是一种容器,用于装数据的,类似数组,但集合的大小可变,开发中常常用到

集合大致可以分为两类,一类是单列集合,另外一列是双列集合

单列集合顾名思义,它存放数据是以单个为一体的,各个数据之间没有关联

双列集合,则是两个数据为一体的,它们之间是有关联的,这也是我们常说的键值对

集合类是用于存储和操作对象组的类库。这些类允许开发人员存储和检索数据,以实现诸如排序、搜索、过滤和映射等功能。

Java集合类主要分为两大类:Collection(单列集合)和Map(双列集合)。

 List系列集合的特点:

添加元素是有序的,可以重复添加,有索引

Set系列集合的特点:

添加元素是无序的,不重复,无索引

Collection接口的一些方法是直接被子类所继承的,所以我们需要先了解单列集合的祖宗类有那些方法

1. public boolean add(E e);

往集合中添加元素

Collection<String> list = new ArrayList<>();
//public boolean add(E e);
//添加元素,成功返回ture
list.add("java");
list.add("mysql");
list.add("spring");
System.out.println(list); //[java, mysql, spring]

 

2.public void clear();

清空集合中的元素

//public void clear();
//清空集合中的元素
list.clear();
System.out.println(list);//[]

 

3.public boolean isEmpty()

判断集合中的元素是否为空

//public boolean isEmpty()
//判断集合是否为空,为空则返回ture,反之则返回false
boolean empty = list.isEmpty();
System.out.println(empty);//ture

 

4.public int size();

判断集合的大小

//public int size();
//获取集合大小
int size = list.size();
System.out.println(size);//3

 

5.public boolean contains(Object obj);

判断集合中是否包含某个元素

//public boolean contains(Object obj);
//判断集合中是否包含某个元素,包含则返回ture
boolean contains = list.contains("java");
System.out.println(contains);//ture

 

6.public E boolean remove(E e);

删除某个元素

//public E boolean remove(E e);
//删除某个元素,如果有多个重复元素,删除查找到的第一个
boolean remove = list.remove("java");
System.out.println(remove);

 

7.public Object[ ] toArray()

将集合转换为数组

//public Object[] toArray()
//把集合替换成数组
String[] array = list.toArray(new String[list.size()]);//转换为字符串数组
System.out.println(Arrays.toString(array)); //[mysql, spring]

 

8.public boolean addAll(E extend Collection e)

主调集合复制参数集合中的数据到主调集合中

//public boolean addAll(E extend Collection e)
//将集合中的元素全部复制到另一个集合中
ArrayList<String> list1 = new ArrayList<>();
list1.add("java");
list1.add("mybatis");
list.addAll(list1);
System.out.println(list);//[mysql, spring, java, mybatis]

 

三.单列集合的遍历

集合的遍历是不能使用for循环的,因为大多数集合都是无序的,且是无索引的,使用for循环必须要有严格的索引要求的

  1. 迭代器
  2. For增强
  3. Lambda表达式

迭代器

Java中的迭代器(Iterator)是一种设计模式,它提供了一种遍历集合对象元素的方法,而不需要知道集合对象底层的表示方式。迭代器是一种对象,它可以遍历并选择序列中的对象,而开发者不需要了解该序列的底层结构。

迭代器通常用于遍历数组、列表、字符串或其他可迭代的数据结构。通过使用迭代器,开发人员可以逐个访问集合中的元素,而无需了解集合的底层表示方式。这有助于降低代码的复杂性,并使代码更易于维护。

迭代器是用来遍历集合的专用方式,数组是没有迭代器的,Java中迭代器的代表是Iterator

使用迭代器首先要创建一个Iterator对象,它可以理解为指针,且是指向集合的第一个元素的指针

使用hasNext()方法可以判断判断当前位置是否有元素存在

next()方法可以取出当前元素,并且将指针移到下一个位置

//使用迭代器遍历
//1.创建迭代器对象,目标是指向集合的第一个元素
Iterator<String> iterator = list.iterator();
//2.使用hasNext()和Next()组合的while循环遍历集合
while(iterator.hasNext()){//判断当前位置是否有元素,有则可以执行循环
    System.out.println(iterator.next());//取出当前元素,并把指针指向下一个元素
}

 

For增强

For增强是这三个方法中遍历集合最简单的,也是推荐使用的,并且它还可以遍历数组

Java中的增强for循环(也称为for-each循环)是一种简化数组和集合遍历的语法结构。它的作用是提供一种更简洁、更清晰的遍历方式,而不需要使用传统的for循环语法。

增强for循环可以遍历所有实现了Iterable接口的集合,以及数组类型。在循环中,每次循环会自动获取下一个元素,并将该元素存储在指定的变量中,可以直接对该元素进行操作。

相比传统的for循环,增强for循环具有简洁、清晰的语法结构,使代码更加优雅。

格式:

for (元素类型  元素变量:数组或集合){

循环体

}

//使用for增强来遍历集合
for(String s:list){
    System.out.println(s);
}

 

如上代码,真的很简洁,推荐使用

Lambda表达式

从JDK8以后之后,提供了一种更简单,更直接的遍历集合的方式

需要使用到Collection的方法forEach()来完成

default  void  forEach(Consumer<?  super  T>  action) 

其中Consumer是一个函数式接口

它其中的方法就是陆续取集合中的元素

//使用Lambda表达式来遍历(原接口)
list.forEach(new Consumer<String>() {
    @Override
    public void accept(String s) {
        System.out.println(s);
    }
});
//lambda表达式优化
list.forEach((s)-> System.out.println(s));
//方法引用优化
list.forEach(System.out::println);

 

值得注意的是consumer接口的实现类依旧使用的是for增强来遍历的集合

小实例:利用集合存储自定义对象

public class CollectionDemo2 {
    public static void main(String[] args) {
        //创建一个Collection集合对象
        Collection<Student> list = new ArrayList<>();
        //添加对象在其中
        list.add(new Student("李华","201",19));
        list.add(new Student("王明","201",18));
        list.add(new Student("李浩","202",22));
        //For增强遍历:简单,快捷
        for(Student s : list){
            System.out.println("学生姓名:"+s.getName()+ ",学生班级:"+s.getClassName()+ ",学生年龄:"+s.getAge());
            //学生姓名:李华,学生班级:201,学生年龄:19
            //学生姓名:王明,学生班级:201,学生年龄:18
            //学生姓名:李浩,学生班级:202,学生年龄:22
        }
    }
}
class Student{
    private String name;
    private String className;
    private int age;
    public Student(String name, String className, int age) {
        this.name = name;
        this.className = className;
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getClassName() {
        return className;
    }
    public void setClassName(String className) {
        this.className = className;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", className='" + className + '\'' +
                ", age=" + age +
                '}';
    }
}

四.List特有方法

List接口是ArrayList和Linked集合的父接口

所以List有的方法,这两个实现类也会有,所以使用的时候就照葫芦画瓢就行了

List集合是支持索引的,所以有很多索引相关的方法,当然Collection拥有的方法,它肯定也有

1.public void add(int index ,E element);

根据索引增加元素

//public void add(int index ,E element);
//在某个索引处插入元素
list.add(2,"vue");
System.out.println(list); //[java, mysql, vue, Html, css]

 

2.public E remove(int index);

根据索引删除数据,并且返回被删除的数据

//public E remove(int index);
//根据索引删除元素,并且返回被删除的元素
list.remove(2);
System.out.println(list);//[java, mysql, Html, css]

 

3.public E get(int index);

根据索引取值

//public E get(int index);
//根据索引取集合中的元素
String s = list.get(2);
System.out.println(s);//Html

 

4.public E set(int index,E element);

改变传入索引中的值,并且返回旧的值

//public E set(int index,E element);
//改变索引的值,并返回旧值
String html = list.set(2, "html");
System.out.println(html);//Html
System.out.println(list);//[java, mysql, html, css]

 

List集合的特有遍历方式

我们说过list集合是线性的,有序的(存取顺序),且是有索引的,所以它除了可以使用Collection接口的三种遍历方式外

还可以使用for循环来遍历,因为List系列都是有索引的

for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
    //java
    //mysql
    //html
    //css
}

 

ArrayList的实现原理

  1. 利用无参构造创造集合,会在底层创建一个默认长度为0的数组
  2. 添加一个元素时,会在底层创建一个新的长度为10的数组

     

  3. 存满时,重新申请数组扩容1.5倍,然后把数据移动过去,新增元素在新数组后面

     

  4. 如果一次添加多个元素,扩充1.5倍还不够,则创建新数组以实际长度为准

LinkedList实现原理

通过拆分LinkedList集合的英文单词也可以看出来它是由什么数据类型构造的

Linked即链表,所以它的实现底层是利用链表实现的

在数据结构中,常见的链表类型有两种,一种是单链表,一种是双向链表

单向链表实现比较简单,只用单个方向,而双向链表则是可以正向反向检索。

链表一个空间存储具体的数据,另外一个空间存储下一个节点的地址,双向链表则是两个空间存储地址

 在LinkedList结构中,使用的是双向链表结构。所以LinkedList的特点就是:

  • 首尾节点操作敏感(操作快)
  • 删除新增节点迅速

示例:利用LinkedList构造队列

队列是一种先进先出的数据结构,所以构造队列只需要删除数据在队首,新增数据在队尾就可以完成

刚好LinkedList是一种首尾操作敏感的数据结构,所以做队列很可以

LinkedList<String> list = new LinkedList<>();
//常见LinkedList方法
//public void addFirst(E e) 在头节点插入元素
list.addFirst("first");
//public void addLast(E e) 在尾节点插入元素
list.addLast("last");
//public E getFirst()  得到第一个节点值
list.getFirst();
//public E getLast() 得到最后一个节点值
list.getLast();
//public E removeFirst() 删除第一个节点,并且返回此值
list.removeFirst();
//public E removeLast() 删除最后一个节点,并且返回此值
list.removeLast();

 

构造队列,出队与入队:

LinkedList<String> queue = new LinkedList<>();
//入队
queue.addLast("1");
queue.addLast("2");
queue.addLast("3");
//出队
queue.removeFirst(); //"1"
System.out.println(queue); //"2"  "3"

 五.Set集合

set集合的特点:无序,添加数据的顺序和取出数据的顺序是不一致的,不重复,无索引

HashSet:无序,不重复,无索引

LinkedHashSet:有序,不重复,无索引

TreeSet:排序,不重复,无索引

Set集合的常用方法也是单列集合中的,并没有其它独有的set方法,所以不过多赘述了

Set<Integer> set = new HashSet<>();
set.add(123);
set.add(365);
set.add(894);
set.add(422);
for (Integer s : set) {
    System.out.println(s);
}//422 123 365 894

 

HashSet底层实现原理

HashSet实现是基于Hash表的思想来实现的

通过对具体的数字进行哈希值计算,最后确定这个值存放的位置

Hash值是一个int类型的变量,Java每个对象都有一个hash值,通过调用HashCode()方法拿到

对象Hash值的特点:

同一个对象调用HashCode获得的值是一样

不同对象调用绝大数情况是不同的,但也会有相同的情况(哈希碰撞)--int类型一共可容纳42亿左右的值,而当有45亿个对象时,必然发生哈希碰撞

在JDK8之前,HashSet实现方式是通过数组+链表的方式实现的,而JDK8之后都是使用数组+链表+红黑树实现的

JDK8之前

  1. 在底层实现中先实现一个长度为16的数组,默认加载因子0.75,
  2. 使用元素的哈希值对数组长度取余,计算出对应存入的位置
  3. 判断当前位置是否为NULL,如果是null直接存入
  4. 不为Null,则表示有数据,调用equals方法进行比较,相等,则不存入(set无重复),不相等,存入数组
  • jdk8之前,新元素存入数组,占老元素位置,老元素挂下面;jdk8之后,直接挂在老元素后面

 当数组长度被占满0.75(默认加载因子)倍后,即16*0.75 =12,就会扩容数组的一倍,16*2 =32

为什么是占满0.75倍就扩容而不是占满后扩容呢?因为当占12个空间后,大概率这12个空间下已经有挂着的数据了,加一起可能已经超过16个了

JDK8之后

JDK8之后HashSet的实现和JDK8实现多了一个红黑树,在少量数据上依旧是数组+链表

当满足:链表长度超过8,且数组长度 大于等于64,自动将链表转为红黑树

注意:是将链表转为红黑树,而不是整体都是红黑树结构,数组表还是存在的,此时应当满足,数组长度 >= 64了

当链表大于8时,使用红黑树来存储,检索更加高效

 拓展

当我们希望两个内容相同的对象,在存储的过程HashSet的过程中,不重复存入

在直接进行存入中,由于对象的初始HashCode是随机的,所以即使内容相同的对象,它们的Hashcode都是不一样的,所以需要更改此类的HashCode方法和equals方法

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Student student = (Student) o;
    return grade == student.grade && Objects.equals(name, student.name) && Objects.equals(sec, student.sec);
}
@Override
public int hashCode() {
    return Objects.hash(name, sec, grade);
}

 

重写后,让此对象更具自己的属性来构造Hashcode,就能保证相同的属性值产生相同的HashCode

Student s1 = new Student("msf", "boy", 18);
Student s2 = new Student("msf", "boy", 18);
System.out.println(s1.hashCode()==s2.hashCode());//true

 LinkedHashSet底层原理

LinkedHashSet的主要特点是有序,不重复,无索引

这里的有序指的是添加元素的顺序,并不是对元素排序

LinkedHashSet依旧是使用的数组+链表+红黑树,但是与HashSet不同的是,LinkedHashSet有一个双链表负责链接存入的元素,也就是首尾指针指向第一个元素和最后一个元素

使用双链表的机制就可以做到存储数据后还能存储插入数据的顺序,由此可知存储数据是哈希表,而存储顺序是双链表;但是这样的方式肯定是更消耗空间的,适用于要求更严苛的情况

如下图:

 如图:LinkedHashSet除了要保存链表结构,还有存储数据的顺序,通过头尾指针可以快速的定位到这些数据的位置

所以在遍历数据时,HashSet时直接遍历hash表,从0号位置开始,然后链表,而LinkedHashSet则是从头指针开始,到尾指针结束,可记录到数据的插入顺序,但是空间开销比HashSet大

TreeSet底层实现原理

Treeset的特点是排序,不重复,无索引

 这里的排序指的是按照某种排序规则进行升序的,实现的数据结构就是红黑树

红黑树是一种很复杂的数据结构,在了解红黑树前必须了解树结构,详细了解红黑树可以看数据结构与算法之二叉树

注意点:在排序基本类型时,TreeSet可以根据数据大小进行排序,而对象结构是不能直接进行排序的,因为TreeSet不知道排序规则

所以针对对象比较TreeSet可以使用两种方式来解决:

  1. 实现Comparable接口
    @Override
        public int compare(Student o1, Student o2) {
            return o1.grade-o2.grade;
        }

     

  2. 调用TreeSet集合有参构造器,设置Comparator
    Set<Student> s = new TreeSet<>(new Comparator<Student>() {
                @Override
                public int compare(Student o1, Student o2) {
                    return o1.getGrade()-o2.getGrade();
                }
            });
    
    //Lambda表达式优化
     Set<Student> s = new TreeSet<>((o1, o2) -> { return o1.getGrade()-o2.getGrade(); });

     

 集合并发的修改异常

使用迭代器遍历集合时,又同时删除集合的数据,程序就会发生修改异常的错误

ArrayList<String> list = new ArrayList<>();
list.add("吴一凡");
list.add("菜许坤");
list.add("顶正");
list.add("小一");
list.add("黄明浩");
list.add("刘一博");
System.out.println(list);//[吴一凡, 菜许坤, 顶正, 小一, 黄明浩, 刘一博]
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
    String name = iterator.next();
    if (name.contains("一")){
        list.remove(name);
    }
}

 

如上代码:需要删除名字中带“一”的人,使用迭代器删除,但是会报错并发修改异常.ConcurrentModificationException

并发即多线程在操作,我们在使用迭代器的时候,进行删除操作是一条线程,而Collection集合也有一个线程在维护集合中的数据

如ArrayList就是当数据有删除时,就会向前进行移动,以确保数组时充盈的

使用for循环就可以明显看出来

ArrayList<String> list = new ArrayList<>();
list.add("吴一凡");
list.add("菜许坤");
list.add("顶正");
list.add("小一");
list.add("刘一博");
list.add("黄明浩");
System.out.println(list);//[吴一凡, 菜许坤, 顶正, 小一, 黄明浩, 刘一博]
for (int i = 0; i < list.size(); i++) {
    if (list.get(i).contains("一")){
        list.remove(list.get(i));
    }
}
System.out.println(list);//[菜许坤, 顶正, 刘一博, 黄明浩]

 

如上,使用for循环可以看到,我们少删除了一个人名,这就是在删除小一时,集合向前移动了一格,导致刘一博在已经删除过的位置,下次循环就跳走了

所以解决方式可以在有删除动作的情况下,执行一个i--,i--就到来删除的前一个元素,在配合for循环的i++就是当前位置,则可以删除掉刘一博

ArrayList<String> list = new ArrayList<>();
list.add("吴一凡");
list.add("菜许坤");
list.add("顶正");
list.add("小一");
list.add("刘一博");
list.add("黄明浩");
System.out.println(list);//[吴一凡, 菜许坤, 顶正, 小一, 黄明浩, 刘一博]
for (int i = 0; i < list.size(); i++) {
    if (list.get(i).contains("一")){
        list.remove(list.get(i));
        i--;
    }
}
System.out.println(list);//[菜许坤, 顶正, 黄明浩]

 

知道了问题的出现,迭代器则可以通过另外一种方式,是官方的一种方法,使用迭代器自带的删除,删除元素,就不会报错了

在迭代器的底层也是执行了类似i--的操作,让指针回来了

ArrayList<String> list = new ArrayList<>();
list.add("吴一凡");
list.add("菜许坤");
list.add("顶正");
list.add("小一");
list.add("刘一博");
list.add("黄明浩");
System.out.println(list);//[吴一凡, 菜许坤, 顶正, 小一, 黄明浩, 刘一博]
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
    String name = iterator.next();
    if (name.contains("一")){
        iterator.remove();
    }
}
System.out.println(list);//[菜许坤, 顶正, 黄明浩]

 

注意点:其它的遍历方式是不能解决这个并发删除异常的,forEach不能拿到迭代器对象,for增强拿不到下标,所以解决方式只能是迭代器,Set系列不能用for循环

 

posted @ 2024-03-30 18:41  回忆也交给时间  阅读(7)  评论(0编辑  收藏  举报