【数据结构】List相关知识的学习【详解篇2】


【提示】:主要还是看代码示例:很多细节知识点都是都藏在代码注释里呢,水平有限,如发现有错,望及时批评指正,万分感谢。

List

可以把List理解成“线性表”,也就是有前后顺序关系的一种数据结构;
线性表可分为两种:一个是顺序表ArrayList/Vetor,一个是链表LinkedList。

泛型(Generic)的基本介绍

泛型引入的背景:为了能够写一个类或方法就能同时支持多种不同类型的对象;

泛型的分类:泛型类和泛型方法。

所谓的泛型,其实就是从语法层面上对Object(对象)进行了一个简单的包装,可以在编译过程中帮助我们自动加上一些编译器的类型检查,自动帮我们完成一些类型转换工作。

public class TestDemo1 {
    //private int[] data;//把这个数组单纯的定义成int类型,定义成int类型意味着当前顺序表里面只能存储整数
    //只存储整数不太合适,按理说顺序表应该是存什么类型的都可以 只存整数未免太狭隘,那么有什么办法让这个类能够去存储很多种不同种不同的数据类型呢?
   //解决这个问题,可以使用泛型这个方式,也可以使用Object
    //《使用Object来解决这一问题》
    private Object[] data=new Object[10];
    private int size;
    public void add(Object elem){
        data[size]=elem;
        size++;
    }
    public Object get(int index){
        return data[index];
    }
    public static void main1(String[] args) {
        //String 也是继承自Object
        //调用add的时候相当于是向上转型,向上转型也可以理解为隐式类型转换
        TestDemo1 testDemo1=new TestDemo1();
        testDemo1.add("你你你");//插入元素
        testDemo1.add("我我我");
        // 调用get的时候返回的是Object,这里需要向下转型,需要强制类型转换
        String str=(String) testDemo1.get(0);//获取下标为0的元素
    }
}

泛型的使用

package java2021_1002;
/**
 * Description:泛型
 */
/*
1、尖括号<>:是泛型的标志
2、E是类型变量(Type Variable),变量名一般大写
*/
public class TestDemo2<E> { //<E>是泛型参数,用于指定具体泛型参数是哪个类型;
    //这个参数相当于是一个形参,需要在真正对该类进行实例化的时候,确定实参。
    //泛型参数常见的名字:E  T Key  Value等
    private E[]data=(E[]) new Object[100];//直接new一个泛型类是不可以的,当前这个data的类型是啥,可以就当成一个Object[],Object具体代表的是哪种类型
    /*需要最终在实例化TestDemo2的时候才能够确定下来。
    E这样的泛型参数是不能被直接实例化的。因为当前还不知道E到底是啥类型,所以可以new一个Object类型
    因为不管E是啥类型,它终究是Object衍生出来的子类,所以可以创建一个Object数组来对应到E这样类型的数组, 直接赋值是不可以的,需要进行类型转换 (E[]) new Object[100]*/
    private int size;
    public void add(E elem){
        data[size]=elem;
        size++;
    }
    public E get(int index){//E是数组里面每个元素的类型
        return data[index];
    }
    //泛型编程类似于一个“模板”  ,以上就相当于是一个模板,基于这套模板就可以往上套各种不同类型的数据,他们可以随着泛型参数自动发生改变,因此也就可以解决不同场景下的问题了
    public static void main(String[] args) {
        //T TestDemo1里面的E类型就代表String类型
        TestDemo2<String> testDemo2=new TestDemo2<>();
        testDemo2.add("他他他");
        testDemo2.add("她她她");
        String str=testDemo2.get(0);//返回值这里直接赋值即可,就不用加强制类型转换了
        TestDemo2<Animal> animalTestDemo2=new TestDemo2<>();
        //可以发现泛型参数不一定非要传String,只要它是一个引用类型,即使是自己创建的类,也可以传进来
        animalTestDemo2.add(new Animal());
        animalTestDemo2.add(new Animal());
        Animal animal=animalTestDemo2.get(0);//
    }
}
package java2021_1002;
/**
 * Description:自己创建一个Animal类
 */
public class Animal {
    private String name;
}

泛型背后作用时期和背后的简单原理

泛型这样的语法是一种编译期的机制,也就是在编译过程中所涉及到的语法。为了方便程序员书写代码以及在编译过程进行一些类型检查操作。

如果编译完成了之后,在运行过程中,是没有泛型的。即编译器在编译代码过程中,直接把泛型参数当成了Objec

只不过编译器自动加上了一些类型转换操作以及类型校验操作。

那如果是int,double这些基础类型(内置类型),如何套用到泛型中呢?

由于内置类型并不是继承自Object,所以如果直接套用泛型“模板”的话,编译是过不了的,image-20211003105758186因为int就是int,它和Object之间是没有关系的,必须是创建的类才是将继承自Object的,而int、double、float这四类八种基础类型并不是我们自己创建的类也不是标准库创建的类而是内置的类型,所以此时无法直接把int放进去,那该怎么办呢?

  • 直接插入不行,我们可以创建一个类,用这个类来表示一个整数,此时创建类表示的就是引用类型了。只不过这个类不需要我们手动,创建标准库已经帮我们创建好了,换句话说,这八种基本类型,标准库都已经帮我们创建好一一对应的类来表示这里面的基本类型了,这里面的类就被称为包装类,包装类就是把前面的八种基本数据类型,用一个类稍微包装一下把它变成了引用类型。下面就进入包装类的学习吧

泛型总结

  1. 泛型是为了解决某些容器、算法等代码的通用性而引入,并且能在编译期间做类型检查.
  2. 泛型利用的是 Object 是所有类的祖先类,并且父类的引用可以指向子类对象的特定而工作。
  3. 泛型是一种编译期间的机制,即 MyArrayList 和 MyArrayList 在运行期间是一个类型。
  4. 泛型是Java中的一种合法语法,标志就是尖括号<>.

包装类(Wrapper Class)

Object 引用可以指向任意类型的对象,但有例外出现了,8 种基本数据类型不是对象,那岂不是刚才的泛型机制要失效了?
实际上也确实如此,为了解决这个问题,java 引入了一类特殊的类,即这 8 种基本数据类型的包装类,在使用过程中,会将类似 int 这样的值包装到一个对象中去。

基本数据类型和包装类直接的对应关系

下面这张表表明了当前的包装类都有哪些:

基本数据类型对应的包装类
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDoublt
charCharacter
booleanBoolean
  • 基本数据类型对应的包装类基本上就是类型的首字母大写,除了Integer和Character。

为什么要有包装类?

java本身是一个面向对象的语言,有时候要处理一些情况,比如将字符串转化成整数,将整数转换成字符串,这种情况下就只能借助包装类进行转换,因为如果它是一个类的话,它肯定有一些方法支持去转换它的,所以,如果让基本类型有对应的包装类的话有一个好处是,以后在处理一些事物的时候,基本类型都是可以去面向对象的,一旦它面向对象了我们就可以用对象当中的方法去处理并解决我们的问题,这也是包装类的用途。

代码示例:关于Integer包装类的使用及装箱拆箱操作

package java2021_1002;
/**
 * Description:包装类
 */
public class TestDemo2<E> {
    private E[]data=(E[]) new Object[100];
    private int size;
    public void add(E elem){
        data[size]=elem;
        size++;
    }
    public E get(int index){//E是数组里面每个元素的类型
        return data[index];
    }
    //泛型编程类似于一个“模板”  ,以上就相当于是一个模板,基于这套模板就可以往上套各种不同类型的数据,他们可以随着泛型参数自动发生改变, 因此也就可以解决不同场景下的问题了
    public static void main(String[] args) {
        //此时保存的是Integer这样的一个包装类,所以这种类型可以作为泛型参数
        TestDemo2<Integer> integerTestDemo2=new TestDemo2<>();
        integerTestDemo2.add(new Integer(10)); //new一个Integer对象,这个对象里面保存的内容就是10这样的一个整数
        Integer num=new Integer(10);//也可以这样写
        integerTestDemo2.add(num);
        //这种操作相当于把int内置类型转换成Integer,我们把这种操作称之为(手动)装箱
         Integer num1=Integer.valueOf(10);//Integer类里面有一个静态方法valueof方法,也通过调用valueof方法,完成(手动)装箱操作
        Integer num2=10;//这种直接把int赋值给Integer的操作,叫做自动装箱
        integerTestDemo2.add(num2);
        //拆箱:把Integer转换成int
        num=integerTestDemo2.get(0);
        int value=num.intValue();//手动拆箱
        int value1=num;//自动拆箱
    }
}

包装类的使用:装箱(boxing)和拆箱(unboxing)

  • 装箱和拆箱有时候也被叫做装包和拆包,所谓的装箱和拆箱操作其实就是类型转换,只不过这个类型一个是内置类型一个是引用类型。
  • 装箱/装包:将简单类型包装为包装类类型
  • 拆箱/拆包:将包装类类型转变为简单类型

可使用帮助手册查看关于Integer:

image-20211010155615811

手动装箱和手动拆箱的代码示例:

 public static void main(String[] args) {
       /*装箱有两种方式:一种是valueof,一种是new构造方法*/
        int i=10;//简单类型
        // 《手动装箱操作》:新建一个 Integer 类型对象,将 i 的值放入对象的某个属性中
        Integer ij=new Integer(i);//原来i只是一个简单类型,new一下之后就变成了一个类了,这叫做装箱操作
        //Integer类里面有一个静态方法valueof方法,也通过调用valueof方法,完成装箱操作
        Integer ii=Integer.valueOf(i);//valueof这个方法就是将一个简单类型包装为它所对应的包装类型,这个过程就也叫做装箱
        // 《手动拆箱操作》:将 Integer 对象中的值取出,放到一个基本数据类型中
        int j = ii.intValue();//如果想把ii整型拆成简单的int类型就调用intValue()
        double d=ii.doubleValue();//如果想把ii整型拆成简单的double类型就调用doubleValue()
        /*拆箱就是看要拆成什么类型就调用它所对应的value方法*/
    
    }

自动装箱和自动拆箱代码示例:自动装箱/拆箱,也就是隐式类型转换

 public static void main(String[] args) {
        Integer a=10;//10就是一个简单类型,把一个int 赋值给Integer,这样写就是自动装箱(自动装箱是编译器赋予包装类的特殊功能,也就是说只有包装类才能使用这样的特点,其他类型是不同通过这种方式进行类型转换的)
        int b=a;//自动拆箱
        /*虽然是自动装箱,但是它的底层调用的还是valueOf方法,自动拆箱底层调用的还是intValue方法*/
      //
    }

可以使用反编译工具javap来查看自动装箱和拆箱的底层原理,用法是:javap -c 文件名(类名) ,它可以做到反汇编java程序。
拓展:代码示例:

 public static void main(String[] args) {
        Integer a=100;//自动装箱
        Integer b=100;
        System.out.println(a==b);
        Integer a1=200;
        Integer b1=200;
        System.out.println(a1==b1);
    }
//打印结果:
true
false

以上代码的结果为何是这样?

自动装箱是在底层依然调用valueOf方法,

鼠标放到valueOf上面,点击ctrl,转到valueOf的源码如图:

image-20211002003320086

image-20211002003353305

所以,如果给定的数据在这个范围内(即:i>=-128 && i<=127),就会执行if语句,那么每次都会在这个下标去取数字,并返回出去。所以a和b里面所存储的都是第一次的100,所以相等,结果为true。
如果给定的数据不在这个范围,if语句就进不去了,就会new一个对象,所以a1和b1里面保存的就是是两个对象的引用,所以不相等,结果为false。

javap 反编译工具

这里我们刚好学习一个 jdk 中一个反编译工具来查看下自动装箱和自动拆箱过程,并且看到这个过程是发生在编译期间的。

javap -c 类名称
Compiled from "Main.java"
public class Main {
  public Main(); 
    Code:
      0:aload_0 
      1: invokespecial #1     // Method java/lang/Object."<init>":()V
      4: return 
  public static void main(java.lang.String[]); 
    Code:
      0: bipush 10 
      2: istore_1 
      3: iload_1 
      4: invokestatic #2     // Method java/lang/Integer.valueOf: 
(I)Ljava/lang/Integer; 
      7: astore_2 
      8: iload_1 
      9: invokestatic #2     // Method java/lang/Integer.valueOf: 
(I)Ljava/lang/Integer; 
      12: astore_3 
      13: aload_2 
      14: invokevirtual #3   // Method java/lang/Integer.intValue:()I 
      17: istore 4 
      19: aload_2 
      20: invokevirtual #3    // Method java/lang/Integer.intValue:()I 
      23: istore 5 
      25: return 
}

  • 编译:是把.java转成.class(例如:编译是把猪肉做成火腿肠)
  • 反编译:是把.class转成.java ,但是反编译不可能100%还原之前的代码。只能看到其中一部分的核心逻辑。
  • 反编译就是我们不能再把火腿肠还原成猪肉但是能从中直到它的大概内容(是猪肉做的)。

List的使用

List(线性表)的常见方法

List官方文档

方法解释
boolean add(E e)尾插e
void add(int index,E element)将e插入到index位置
boolean addAll(Collection<?extend E>c)尾插c中的元素
E remove(int index)删除index位置元素
boolean remove(Object o)删除遇到的第一个o
E get(int index)获取下标index位置元素
E set(int index,E element)将下标index位置元素设置为element
void clear()清空
boolean contains(Object o)判断o是否在线性表中
int indexOf(Object o)返回第一个o所在下标
int lastIndexOf(Object o)返回最后一个o的下标
List subList(int fromIndex,int toIndex)截取部分list

以上方法中的个别参数所代表的意思解释如下:

  • E:表示< >里面的数据类型
  • e:element(元素)
  • index:下标
  • o:表示对象
  • c:表示集合

image-20211010103702226

代码示例:

package java2021_1002;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

/**
 * Created by Sun
 * Description:List接口
 * User:Administrator
 * Date:2021-10-03
 * Time:15:08
 */
public class TestList {
        //1、创建List实例
    public static void main(String[] args) {
        List<String> list=new ArrayList<>();// list有它自己的方法,ArrayList也有它自己的方法,并且ArrayList底层是一个数组
        //2、新增元素,尾插e,e是element(元素)
        list.add("北京");//使用add方法,默认是放到了这个数组的最后位置
        list.add("上海");
        list.add("广州");
        list.add("深圳");
        list.add("杭州");
        //3、直接打印整个list,按照添加顺序打印
        System.out.println("尾插e:"+list);//可以直接打印,因为java集合类都是已经默认把toString方法给写好了,所以只要直接打印就能看到完整的内容
        // 4、将e插入到index位置,index是下标,e是element
        list.add(1,"厦门");
        System.out.println("将e插入到index位置:"+list);
        //5、把另一个集合类的元素,放到list当中
        List<String> list1=new LinkedList<>();
        list1.add("香港");
        list1.add("澳门");
        list.addAll(list1);
        System.out.println("把另一个集合类的元素,放到list当中:"+list);
        //6、删除index位置元素,这个remove传过去的是一个整数
        System.out.println("下标为1的元素是:"+list.remove(1));
        System.out.println("删除index位置元素:"+list);
        //7、删除对应的字符串,这个remove传过去的是一个对象
        System.out.println(list.remove("澳门"));
        System.out.println("删除遇到的第一个o"+list);//如果有多个对象,就是删除从0下标开始的第一个澳门对象
        //8、按照下标来访问元素
        System.out.println("打印下标为0的元素:"+list.get(0));//打印下标为0的元素
        //9、根据下标来修改元素(下标不能够越界),将某一个下标更新成你想更新的值
        list.set(2,"郑州");
        System.out.println("根据下标来修改元素:"+list);
        //10、判断o是否在线性表中
        System.out.println(list.contains("北京"));
        System.out.println(list.contains("河南"));
        //11、返回第一个o所在下标,如果有多个对象,就是从0下标开始的第一个北京所在的下标
        System.out.println(list.indexOf("北京"));
        list.add("北京");
        System.out.println("尾插e:"+list);
        //12、返回最后一个o的下标,如果有多个对象,就是从0下标开始的最后一个北京所在的下标
        System.out.println(list.lastIndexOf("北京"));
        System.out.println(list.lastIndexOf("南京"));//如果没有这个对象就返回-1
        //13、使用subList来获取子序列,截取部分 [1, 4)
        System.out.println("获取子序列:"+list.subList(1,4));//前闭后开区间
        System.out.println("打印list:"+list);
        //14、subList的返回值是List<E>,所以还可以使用两个引用同时指向一个对象的方法,通过其中的某一个引用修改这个对象的值,那么另一个引用访问的时候也会被改
        List<String> list2=list.subList(1,3);
        System.out.println(list2);
        //将list2的0下标的元素修改成河南,此时list2的0下标对应的是list的1下标
        list2.set(0,"河南");
        System.out.println("访问list2"+list2);
        System.out.println("访问list:"+list);
        //15、清空
        list.clear();
        System.out.println("清空list:"+list);
        //使用for循环来访问每个元素
        for (String s : list) {//使用for循环的第一种方式for each 来遍历,此时取到的每一个s这样的一个引用,它将会分别指向list中的每一个元素
            //list接口最上层实现的一个接口叫做Iterable,只要某个类实现了Iterable接口,就可以使用for each循环
            System.out.println("for each 循环遍历:" + s);
        }
        for (int i = 0; i < list.size(); i++) {//使用for循环的第二种方式:传统for循环来遍历每一个list
            //虽然List接口的常见方法当中没有size()方法,但是因为list实现了Collection接口,所以Collection中的操作,List也能使用,Collection接口中有size()方法。
            System.out.println("传统for循环遍历:" + list.get(i));
        }
         //16、可以使用构造方法来重新构造出新的List对象
        List<String> list3=new ArrayList<>(list);//把list的内容拷贝一份得到了一个新的对象list3
        System.out.println(list3);
        //这样的一个操作是深拷贝还是浅拷贝呢?看修改list会不会影响list3
        list.set(0,"南京");//将list当中的0号元素改为南京
        System.out.println(list);
        System.out.println(list3);//打印结果:没有影响
        //因为List的泛型参数是String,String是不可变参数类型,所以要想验证是不是深拷贝,需要给List泛型参数填一个可变对象的类型(如:Integer)才可以。
        //17、清空
        list.clear();
        System.out.println("清空list:"+list);
    }
}

打印结果如下:
image-20211010115954182

ArrayList(顺序表)的常见方法

ArrayList官方文档

方法解释
ArrayList()无参构造
ArrayList(Collection<? extends E>c)利用其它Collection构建ArrayList
ArrayList(int initialCapacity)指定顺序表初始容量

代码示例:

package java2021_1002;

import java.util.ArrayList;
import java.util.List;

/**
 * Description:ArrayList(顺序表)的常见方法
 */
public class TestArrayList {
    //使用构造方法来重新构造出新的List对象
    //1、无参构造
    List<Integer> list1=new ArrayList<>();
    //2、利用其它Collection构建ArrayList
    ArrayList<Integer> list2=new ArrayList<>(list1);//传一个集合list1,相当于new了一个ArrayList
    ArrayList<Integer> list3=new ArrayList<>(new ArrayList<>());
    //3、指定顺序表初始容量
    ArrayList<Integer> list4=new ArrayList<>(10);//此时顺序表的容量就为10
}

LinkedList(链表)的常见方法

LinkedList官方文档

方法解释
LinkedList()无参构造

代码示例:

package java2021_1002;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

/**
 * Description:LinkedList(顺序表)的常见方法
 */
public class TestLinkedList {
    //可以使用构造方法来重新构造出新的List对象
    //1、无参构造
    List<Integer> list1=new LinkedList<>();
    LinkedList<Integer> list4=new  LinkedList<>();// LinkedList底层是一个双向链表,链表没有大小,所以不能传一个带有整数的参数
    //2、利用其它Collection构建ArrayList
    LinkedList<Integer> list2=new LinkedList<>(list1);//传一个集合list1,相当于new了一个 LinkedList
    LinkedList<Integer> list3=new LinkedList<>(new  LinkedList<>());
}

练习题

练习题1

题目描述:某校有若干学生(学生对象放在一个List中),每个学生有一个姓名(String)、班级(String)和考试成绩属性(double),某次考试结束后,每个学生都获得了一个考试成绩。遍历list集合,并把学生对象属性打印出来。

代码:

package java2021_1002;

import java.util.ArrayList;

class Student{
    //设置属性
    private String name;
    private String classes;
    private double score;
    //提供构造方法
    public Student(String name, String classes, double score) {
        this.name = name;
        this.classes = classes;
        this.score = score;
    }
    //因为是private的,所以要提供getter、setter方法

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getClasses() {
        return classes;
    }

    public void setClasses(String classes) {
        this.classes = classes;
    }

    public double getScore() {
        return score;
    }

    public void setScore(double score) {
        this.score = score;
    }
    //打印出学生的所有属性,需要重写toString方法
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", classes='" + classes + '\'' +
                ", score=" + score +
                '}';
    }
}
public class TestExercises1 {
    public static void main(String[] args) {
        ArrayList<Student> list1=new ArrayList<>();//这里我们可以知道,类型不一定非得是Integer、String等
        list1.add(new Student("小小","1班",89));
        list1.add(new Student("大大","2班",77));
        list1.add(new Student("多多","1班",99));
        //使用重写的toString方法打印
        System.out.println(list1);//
        System.out.println("====================");
        //也可以使用for循环打印
        for (Student student:list1) {
            System.out.println(student);
        }
    }
}

打印结果:
image-20211010134001397

练习题2

题目描述:有一个List当中存放的是整型数据,要求使用Collections.sort对List进行排序。

代码:

package java2021_1002;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;

public class TestExercises2 {
    public static void main(String[] args) {
        ArrayList<Integer> list=new ArrayList<>();
        list.add(9);
        list.add(18);
        list.add(2);
        list.add(5);
        list.add(11);
        System.out.println("排序前:"+list);
        Collections.sort(list);
        System.out.println("排序后:"+list);
    }
}

打印结果:image-20211010134905870

练习题3

删除第一个字符串当中出现的第二个字符串中的字符。

如:

String str1="welcome to beijing";
String str2="come";
输出结果:wl t bijing

代码:使用集合的方式

package java2021_1002;

import java.util.ArrayList;
import java.util.List;

public class TestExercises3 {
    public static void func(String str1,String str2){
        if(str1==null || str2==null){
            return;
        }
    List<Character> list=new ArrayList<>();//char的包装类是Character
    for(int i=0;i<str1.length();i++){//遍历str1里面的每一个字符
        char ch=str1.charAt(i);
        if(!str2.contains(ch+"")){
            list.add(ch);
        }
    }
    //打印
        System.out.println(list);
    //使用for循环打印
        for (char c:list) {
            System.out.print(c);
        }
    }
    public static void main(String[] args) {
        String str1="welcome to beijing";
        String str2="come";
        func(str1,str2);//调用func方法,并传参
        //func("welcome to beijing","come");//也可以直接传字符串
    }
}

打印结果:
image-20211010143614814

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值