从零开始的八股文

一、Java基础篇

1.接口和抽象类的区别

相似点:

(1)接口和抽象类都不能被实例化

(2)实现接口或继承抽象类的普通子类都必须实现这些抽象方法

不同点:

(1)抽象类可以包含普通方法和代码块,接口里只能包含抽象方法,静态方法和默认方法

(2)抽象类可以有构造方法,而接口没有

(3)在接口中定义的成员变量默认会加上: public static final修饰

接口详述

2.1 概述

我们已经学完了抽象类,抽象类中可以用抽象方法,也可以有普通方法,构造方法,成员变量等。那么什么是接口呢?接口是更加彻底的抽象,JDK7之前,包括JDK7,接口中全部是抽象方法。接口同样是不能创建对象的

2.2 定义格式
 //接口的定义格式:
 interface 接口名称{
     // 抽象方法
 }
 ​
 // 接口的声明:interface
 // 接口名称:首字母大写,满足“驼峰模式”
2.3 接口成分的特点

在JDK7,包括JDK7之前,接口中的只有包含:抽象方法和常量

2.3.1.抽象方法

注意:接口中的抽象方法默认会自动加上public abstract修饰程序员无需自己手写!! ​ 按照规范:以后接口中的抽象方法建议不要写上public abstract。因为没有必要啊,默认会加上。

2.3.2 常量

在接口中定义的成员变量默认会加上: public static final修饰。也就是说在接口中定义的成员变量实际上是一个常量。这里是使用public static final修饰后,变量值就不可被修改,并且是静态化的变量可以直接用接口名访问,所以也叫常量。常量必须要给初始值。常量命名规范建议字母全部大写,多个单词用下划线连接。

2.3.3 案例演示
 public interface InterF {
     // 抽象方法!
     //    public abstract void run();
     void run();
 ​
     //    public abstract String getName();
     String getName();
 ​
     //    public abstract int add(int a , int b);
     int add(int a , int b);
 ​
 ​
     // 它的最终写法是:
     // public static final int AGE = 12 ;
     int AGE  = 12; //常量
     String SCHOOL_NAME = "黑马程序员";
 ​
 }
2.4 基本的实现
2.4.1 实现接口的概述

类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类,也可以称为接口的子类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements关键字。

2.4.2 实现接口的格式
 /**接口的实现:
     在Java中接口是被实现的,实现接口的类称为实现类。
     实现类的格式:*/
 class 类名 implements 接口1,接口2,接口3...{
 ​
 }

从上面格式可以看出,接口是可以被多实现的。大家可以想一想为什么呢?

2.4.3 类实现接口的要求和意义
  1. 必须重写实现的全部接口中所有抽象方法。

  2. 如果一个类实现了接口,但是没有重写完全部接口的全部抽象方法,这个类也必须定义成抽象类。

  3. 意义:接口体现的是一种规范,接口对实现类是一种强制性的约束,要么全部完成接口申明的功能,要么自己也定义成抽象类。这正是一种强制性的规范。

2.4.4 类与接口基本实现案例

假如我们定义一个运动员的接口(规范),代码如下:

 /**
    接口:接口体现的是规范。
  * */
 public interface SportMan {
     void run(); // 抽象方法,跑步。
     void law(); // 抽象方法,遵守法律。
     String compittion(String project);  // 抽象方法,比赛。
 }

接下来定义一个乒乓球运动员类,实现接口,实现接口的实现类代码如下:

 package com.itheima._03接口的实现;
 /**
  * 接口的实现:
  *    在Java中接口是被实现的,实现接口的类称为实现类。
  *    实现类的格式:
  *      class 类名 implements 接口1,接口2,接口3...{
  *
  *
  *      }
  * */
 public class PingPongMan  implements SportMan {
     @Override
     public void run() {
         System.out.println("乒乓球运动员稍微跑一下!!");
     }
 ​
     @Override
     public void law() {
         System.out.println("乒乓球运动员守法!");
     }
 ​
     @Override
     public String compittion(String project) {
         return "参加"+project+"得金牌!";
     }
 }

测试代码

 public class TestMain {
     public static void main(String[] args) {
         // 创建实现类对象。
         PingPongMan zjk = new PingPongMan();
         zjk.run();
         zjk.law();
         System.out.println(zjk.compittion("全球乒乓球比赛"));
 ​
     }
 }
2.4.5 类与接口的多实现案例

类与接口之间的关系是多实现的,一个类可以同时实现多个接口。

首先我们先定义两个接口,代码如下:

 /** 法律规范:接口*/
 public interface Law {
     void rule();
 }
 ​
 /** 这一个运动员的规范:接口*/
 public interface SportMan {
     void run();
 }
 ​

然后定义一个实现类:

 /**
  * Java中接口是可以被多实现的:
  *    一个类可以实现多个接口: Law, SportMan
  *
  * */
 public class JumpMan implements Law ,SportMan {
     @Override
     public void rule() {
         System.out.println("尊长守法");
     }
 ​
     @Override
     public void run() {
         System.out.println("训练跑步!");
     }
 }

从上面可以看出类与接口之间是可以多实现的,我们可以理解成实现多个规范,这是合理的。

2.5 接口与接口的多继承

Java中,接口与接口之间是可以多继承的:也就是一个接口可以同时继承多个接口。大家一定要注意:

类与接口是实现关系

接口与接口是继承关系

接口继承接口就是把其他接口的抽象方法与本接口进行了合并。

案例演示:

 public interface Abc {
     void go();
     void test();
 }
 ​
 /** 法律规范:接口*/
 public interface Law {
     void rule();
     void test();
 }
 ​
  *
  *  总结:
  *     接口与类之间是多实现的。
  *     接口与接口之间是多继承的。
  * */
 public interface SportMan extends Law , Abc {
     void run();
 }
2.6扩展:接口的细节

不需要背,只要当idea报错之后,知道如何修改即可。

关于接口的使用,以下为语法上要注意的细节,虽然条目较多,但若理解了抽象的本质,无需死记硬背。

  1. 当两个接口中存在相同抽象方法的时候,该怎么办?

只要重写一次即可。此时重写的方法,既表示重写1接口的,也表示重写2接口的。

  1. 实现类能不能继承A类的时候,同时实现其他接口呢?

继承的父类,就好比是亲爸爸一样 实现的接口,就好比是干爹一样 可以继承一个类的同时,再实现多个接口,只不过,要把接口里面所有的抽象方法,全部实现。

  1. 实现类能不能继承一个抽象类的时候,同时实现其他接口呢?

实现类可以继承一个抽象类的同时,再实现其他多个接口,只不过要把里面所有的抽象方法全部重写。

  1. 实现类Zi,实现了一个接口,还继承了一个Fu类。假设在接口中有一个方法,父类中也有一个相同的方法。子类如何操作呢?

处理办法一:如果父类中的方法体,能满足当前业务的需求,在子类中可以不用重写。 处理办法二:如果父类中的方法体,不能满足当前业务的需求,需要在子类中重写。

  1. 如果一个接口中,有10个抽象方法,但是我在实现类中,只需要用其中一个,该怎么办?

可以在接口跟实现类中间,新建一个中间类(适配器类) 让这个适配器类去实现接口,对接口里面的所有的方法做空重写。 让子类继承这个适配器类,想要用到哪个方法,就重写哪个方法。 因为中间类没有什么实际的意义,所以一般会把中间类定义为抽象的,不让外界创建对象

抽象类详述

1.1 概述
1.1.1 抽象类引入
  • 抽象方法 : 没有方法体的方法。

  • 抽象类:包含抽象方法的类。

1.2 abstract使用格式

abstract是抽象的意思,用于修饰方法方法和类,修饰的方法是抽象方法,修饰的类是抽象类。

1.2.1 抽象方法

使用abstract 关键字修饰方法,该方法就成了抽象方法,抽象方法只包含一个方法名,而没有方法体。

定义格式:

 修饰符 abstract 返回值类型 方法名 (参数列表);

代码举例:

 public abstract void run();
1.2.2 抽象类

如果一个类包含抽象方法,那么该类必须是抽象类。注意:抽象类不一定有抽象方法,但是有抽象方法的类必须定义成抽象类。

定义格式:

 abstract class 类名字 { 
   
 }

代码举例:

 public abstract class Animal {
     public abstract void run();
 }
1.2.3 抽象类的使用

要求:继承抽象类的子类必须重写父类所有的抽象方法。否则,该子类也必须声明为抽象类。

代码举例:

 // 父类,抽象类
 abstract class Employee {
     private String id;
     private String name;
     private double salary;
     
     public Employee() {
     }
     
     public Employee(String id, String name, double salary) {
         this.id = id;
         this.name = name;
         this.salary = salary;
     }
     
     // 抽象方法
     // 抽象方法必须要放在抽象类中
     abstract public void work();
 }
 ​
 // 定义一个子类继承抽象类
 class Manager extends Employee {
     public Manager() {
     }
     public Manager(String id, String name, double salary) {
         super(id, name, salary);
     }
     // 2.重写父类的抽象方法
     @Override
     public void work() {
         System.out.println("管理其他人");
     }
 }
 ​
 // 定义一个子类继承抽象类
 class Cook extends Employee {
     public Cook() {
     }
     public Cook(String id, String name, double salary) {
         super(id, name, salary);
     }
     @Override
     public void work() {
         System.out.println("厨师炒菜多加点盐...");
     }
 }
 ​
 // 测试类
 public class Demo10 {
     public static void main(String[] args) {
         // 创建抽象类,抽象类不能创建对象
         // 假设抽象类让我们创建对象,里面的抽象方法没有方法体,无法执行.所以不让我们创建对象
 //      Employee e = new Employee();
 //      e.work();
         
         // 3.创建子类
         Manager m = new Manager();
         m.work();
         
         Cook c = new Cook("ap002", "库克", 1);
         c.work();
     }
 }

此时的方法重写,是子类对父类抽象方法的完成实现,我们将这种方法重写的操作,也叫做实现方法

1.3 抽象类的特征

抽象类的特征总结起来可以说是 有得有失

有得:抽象类得到了拥有抽象方法的能力。

有失:抽象类失去了创建对象的能力。

其他成员(构造方法,实例方法,静态方法等)抽象类都是具备的。

1.4 抽象类的细节

不需要背,只要当idea报错之后,知道如何修改即可。

关于抽象类的使用,以下为语法上要注意的细节,虽然条目较多,但若理解了抽象的本质,无需死记硬背。

  1. 抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。

    理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。

  2. 抽象类中,可以有构造方法,是供子类创建对象时,初始化父类成员使用的。

    理解:子类的构造方法中,有默认的super(),需要访问父类构造方法。

  3. 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。

    理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。

  4. 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则子类也必须定义成抽象类,编译无法通过而报错。

    理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。

  5. 抽象类存在的意义是为了被子类继承。

    理解:抽象类中已经实现的是模板中确定的成员,抽象类不确定如何实现的定义成抽象方法,交给具体的子类去实现。

1.5 抽象类存在的意义

抽象类存在的意义是为了被子类继承,否则抽象类将毫无意义。抽象类可以强制让子类,一定要按照规定的格式进行重写。

2.重载和重写的区别

重载发生在同一个类中,方法名相同、参数列表、返回类型、权限修饰符可以不同

重写发生在子类中,方法名相、参数列表、返回类型都相同,权限修饰符要大于父类方法,声明异常范围要小于父类方法,但是final和private修饰的方法不可重写

重写详述

概念

方法重写 :子类中出现与父类一模一样的方法时(返回值类型,方法名和参数列表都相同),会出现覆盖效果,也称为重写或者复写。声明不变,重新实现

使用场景与案例

发生在子父类之间的关系。 子类继承了父类的方法,但是子类觉得父类的这方法不足以满足自己的需求,子类重新写了一个与父类同名的方法,以便覆盖父类的该方 法。

例如:我们定义了一个动物类代码如下:

 public class Animal  {
     public void run(){
         System.out.println("动物跑的很快!");
     }
     public void cry(){
         System.out.println("动物都可以叫~~~");
     }
 }

然后定义一个猫类,猫可能认为父类cry()方法不能满足自己的需求

代码如下:

 public class Cat extends Animal {
     public void cry(){
         System.out.println("我们一起学猫叫,喵喵喵!喵的非常好听!");
     }
 }
 ​
 public class Test {
     public static void main(String[] args) {
         // 创建子类对象
         Cat ddm = new Cat();
         // 调用父类继承而来的方法
         ddm.run();
         // 调用子类重写的方法
         ddm.cry();
     }
 }
@Override重写注解
  • @Override:注解,重写注解校验!

  • 这个注解标记的方法,就说明这个方法必须是重写父类的方法,否则编译阶段报错。

  • 建议重写都加上这个注解,一方面可以提高代码的可读性,一方面可以防止重写出错!

    加上后的子类代码形式如下:

     public class Cat extends Animal {
          // 声明不变,重新实现
         // 方法名称与父类全部一样,只是方法体中的功能重写写了!
         @Override
         public void cry(){
             System.out.println("我们一起学猫叫,喵喵喵!喵的非常好听!");
         }
     }
注意事项
  1. 方法重写是发生在子父类之间的关系。

  2. 子类方法覆盖父类方法,必须要保证权限大于等于父类权限。

  3. 子类方法覆盖父类方法,返回值类型、函数名和参数列表都要一模一样。

重载详述

方法重载
  • 方法重载概念

    方法重载指同一个类中定义的多个方法之间的关系,满足下列条件的多个方法相互构成重载

    • 多个方法在同一个类中

    • 多个方法具有相同的方法名

    • 多个方法的参数不相同,类型不同或者数量不同

  • 注意:

    • 重载仅对应方法的定义,与方法的调用无关,调用方式参照标准格式

    • 重载仅针对同一个类中方法的名称与参数进行识别,与返回值无关,换句话说不能通过返回值来判定两个方法是否相互构成重载

  • 正确范例:

     public class MethodDemo {
         public static void fn(int a) {
             //方法体
         }
         public static int fn(double a) {
             //方法体
         }
     }
     ​
     public class MethodDemo {
         public static float fn(int a) {
             //方法体
         }
         public static int fn(int a , int b) {
             //方法体
         }
     }
  • 错误范例:

     public class MethodDemo {
         public static void fn(int a) {
             //方法体
         }
         public static int fn(int a) {   /*错误原因:重载与返回值无关*/
             //方法体
         }
     }
     ​
     public class MethodDemo01 {
         public static void fn(int a) {
             //方法体
         }
     } 
     public class MethodDemo02 {
         public static int fn(double a) { /*错误原因:这是两个类的两个fn方法*/
             //方法体
         }
     }

3.==和equals的区别

比较基本类型,比较的是值,比较引用类型,比较的是内存地址

equlas是Object类的方法,本质上与==一样,但是有些类重写了equals方法,比如String的equals被重写后,比较的是字符值,另外重写了equlas后,也必须重写hashcode()方法

==的作用

  • 比较基本数据类型:比较的是具体的值

  • 比较引用数据类型:比较的是对象地址值

equals方法的作用

  • 方法介绍

     public boolean equals(String s)     比较两个字符串内容是否相同、区分大小写
  • 示例代码

     public class StringDemo02 {
         public static void main(String[] args) {
             //构造方法的方式得到对象
             char[] chs = {'a', 'b', 'c'};
             String s1 = new String(chs);
             String s2 = new String(chs);
     ​
             //直接赋值的方式得到对象
             String s3 = "abc";
             String s4 = "abc";
     ​
             //比较字符串对象地址是否相同
             System.out.println(s1 == s2);
             System.out.println(s1 == s3);
             System.out.println(s3 == s4);
             System.out.println("--------");
     ​
             //比较字符串内容是否相同
             System.out.println(s1.equals(s2));
             System.out.println(s1.equals(s3));
             System.out.println(s3.equals(s4));
         }
     }

4.异常处理机制

(1)使用try、catch、finaly捕获异常,finaly中的代码一定会执行,捕获异常后程序会继续执行

(2)使用throws声明该方法可能会抛出的异常类型,出现异常后,程序终止

try、catch、finaly、throws的实例

 try{
      编写可能会出现异常的代码
 }catch(异常类型A  e){  当try中出现A类型异常,就用该catch来捕获.
      处理异常的代码
      //记录日志/打印异常信息/继续抛出异常
 }catch(异常类型B  e){  当try中出现B类型异常,就用该catch来捕获.
      处理异常的代码
      //记录日志/打印异常信息/继续抛出异常
 }

 public class TryCatchDemo4 {
     public static void main(String[] args) {
         try {
             read("a.txt");
         } catch (FileNotFoundException e) {
             //抓取到的是编译期异常  抛出去的是运行期 
             throw new RuntimeException(e);
         } finally {
             System.out.println("不管程序怎样,这里都将会被执行。");
         }
         System.out.println("over");
     }
     /*
      *
      * 我们 当前的这个方法中 有异常  有编译期异常
      */
     public static void read(String path) throws FileNotFoundException {
         if (!path.equals("a.txt")) {//如果不是 a.txt这个文件 
             // 我假设  如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常  throw
             throw new FileNotFoundException("文件不存在");
         }
     }
 }

演示下throw的使用

 public class ThrowDemo {
     public static void main(String[] args) {
         //创建一个数组 
         int[] arr = {2,4,52,2};
         //根据索引找对应的元素 
         int index = 4;
         int element = getElement(arr, index);
 ​
         System.out.println(element);
         System.out.println("over");
     }
     /*
      * 根据 索引找到数组中对应的元素
      */
     public static int getElement(int[] arr,int index){ 
         //判断  索引是否越界
         if(index<0 || index>arr.length-1){
              /*
              判断条件如果满足,当执行完throw抛出异常对象后,方法已经无法继续运算。
              这时就会结束当前方法的执行,并将异常告知给调用者。这时就需要通过异常来解决。 
               */
              throw new ArrayIndexOutOfBoundsException("哥们,角标越界了```");
         }
         int element = arr[index];
         return element;
     }
 }

注意:如果产生了问题,我们就会throw将问题描述类即异常进行抛出,也就是将问题返回给该方法的调用者。

那么对于调用者来说,该怎么处理呢?一种是进行捕获处理,另一种就是继续讲问题声明出去,使用throws声明处理。

异常注意事项

  • 运行时异常被抛出可以不处理。即不捕获也不声明抛出。

  • 如果父类抛出了多个异常,子类覆盖父类方法时,只能抛出相同的异常或者是他的子集。

  • 父类方法没有抛出异常,子类覆盖父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不能声明抛出

  • 当多异常处理时,捕获处理,前边的类不能是后边类的父类

  • 在try/catch后可以追加finally代码块,其中的代码一定会被执行,通常用于资源回收。

自定义异常的练习

要求:我们模拟登陆操作,如果用户名已存在,则抛出异常并提示:亲,该用户名已经被注册。

首先定义一个登陆异常类LoginException:

 // 业务逻辑异常
 public class LoginException extends Exception {
     /**
      * 空参构造
      */
     public LoginException() {
     }
 ​
     /**
      *
      * @param message 表示异常提示
      */
     public LoginException(String message) {
         super(message);
     }
 }

模拟登陆操作,使用数组模拟数据库中存储的数据,并提供当前注册账号是否存在方法用于判断。

 public class Demo {
     // 模拟数据库中已存在账号
     private static String[] names = {"bill","hill","jill"};
    
     public static void main(String[] args) {     
         //调用方法
         try{
             // 可能出现异常的代码
             checkUsername("nill");
             System.out.println("注册成功");//如果没有异常就是注册成功
         } catch(LoginException e) {
             //处理异常
             e.printStackTrace();
         }
     }
 ​
     //判断当前注册账号是否存在
     //因为是编译期异常,又想调用者去处理 所以声明该异常
     public static boolean checkUsername(String uname) throws LoginException {
         for (String name : names) {
             if(name.equals(uname)){//如果名字在这里面 就抛出登陆异常
                 throw new LoginException("亲"+name+"已经被注册了!");
             }
         }
         return true;
     }
 }

5.HashMap原理

1.HashMap在Jdk1.8以后是基于数组+链表+红黑树来实现的,特点是,key不能重复,可以为null,线程不安全

2.HashMap的扩容机制:

HashMap的默认容量为16,默认的负载因子为0.75,当HashMap中元素个数超过容量乘以负载因子的个数时,就创建一个大小为前一次两倍的新数组,再将原来数组中的数据复制到新数组中。当数组长度到达64且链表长度大于8时,链表转为红黑树

3.HashMap存取原理:

(1)计算key的hash值,然后进行二次hash,根据二次hash结果找到对应的索引位置

(2)如果这个位置有值,先进性equals比较,若结果为true则取代该元素,若结果为false,就使用高低位平移法将节点插入链表(JDK8以前使用头插法,但是头插法在并发扩容时可能会造成环形链表或数据丢失,而高低位平移发会发生数据覆盖的情况)

6.想要线程安全的HashMap怎么办?

(1)使用ConcurrentHashMap

(2)使用HashTable

(3)Collections.synchronizedHashMap()方法

7.ConcurrentHashMap原如何保证的线程安全?

JDK1.7:使用分段锁,将一个Map分为了16个段,每个段都是一个小的hashmap,每次操作只对其中一个段加锁

JDK1.8:采用CAS+Synchronized保证线程安全,每次插入数据时判断在当前数组下标是否是第一次插入,是就通过CAS方式插入,然后判断f.hash是否=-1,是的话就说明其他线程正在进行扩容,当前线程也会参与扩容;删除方法用了synchronized修饰,保证并发下移除元素安全

8.HashTable与HashMap的区别

(1)HashTable的每个方法都用synchronized修饰,因此是线程安全的,但同时读写效率很低

(2)HashTable的Key不允许为null

(3)HashTable只对key进行一次hash,HashMap进行了两次Hash

(4)HashTable底层使用的数组加链表

9.TreeMap集合

TreeMap集合概述和特点【理解】

  • TreeMap底层是红黑树结构

  • 依赖自然排序或者比较器排序,对键进行排序

  • 如果键存储的是自定义对象,需要实现Comparable接口或者在创建TreeMap对象时候给出比较器排序规则

10.Map

Map集合的基本功能

  • 方法介绍

    方法名说明
    V put(K key,V value)添加元素
    V remove(Object key)根据键删除键值对元素
    void clear()移除所有的键值对元素
    boolean containsKey(Object key)判断集合是否包含指定的键
    boolean containsValue(Object value)判断集合是否包含指定的值
    boolean isEmpty()判断集合是否为空
    int size()集合的长度,也就是集合中键值对的个数

Map集合的获取功能

  • 方法介绍

    方法名说明
    V get(Object key)根据键获取值
    Set<K> keySet()获取所有键的集合
    Collection<V> values()获取所有值的集合
    Set<Map.Entry<K,V>> entrySet()获取所有键值对对象的集合

Map集合的遍历(方式1)

 public class MapDemo01 {
     public static void main(String[] args) {
         //创建集合对象
         Map<String, String> map = new HashMap<String, String>();
 ​
         //添加元素
         map.put("张无忌", "赵敏");
         map.put("郭靖", "黄蓉");
         map.put("杨过", "小龙女");
 ​
         //获取所有键的集合。用keySet()方法实现
         Set<String> keySet = map.keySet();
         //遍历键的集合,获取到每一个键。用增强for实现
         for (String key : keySet) {
             //根据键去找值。用get(Object key)方法实现
             String value = map.get(key);
             System.out.println(key + "," + value);
         }
     }
 }

Map集合的遍历(方式2)

 public class MapDemo02 {
     public static void main(String[] args) {
         //创建集合对象
         Map<String, String> map = new HashMap<String, String>();
 ​
         //添加元素
         map.put("张无忌", "赵敏");
         map.put("郭靖", "黄蓉");
         map.put("杨过", "小龙女");
 ​
         //获取所有键值对对象的集合
         Set<Map.Entry<String, String>> entrySet = map.entrySet();
         //遍历键值对对象的集合,得到每一个键值对对象
         for (Map.Entry<String, String> me : entrySet) {
             //根据键值对对象获取键和值
             String key = me.getKey();
             String value = me.getValue();
             System.out.println(key + "," + value);
         }
     }
 }

11.ArrayList和LinkedList的区别(有源码分析)

ArratList的底层使用动态数组,默认容量为10,当元素数量到达容量时,生成一个新的数组,大小为前一次的1.5倍,然后将原来的数组copy过来;因为数组在内存中是连续的地址,所以ArrayList查找数据更快,由于扩容机制添加数据效率更低

LinkedList的底层使用链表,在内存中是离散的,没有扩容机制;LinkedList在查找数据时需要从头遍历,所以查找慢,但是添加数据效率更高

ArrayList源码分析:

核心步骤:

  1. 创建ArrayList对象的时候,他在底层先创建了一个长度为0的数组。

    数组名字:elementDate,定义变量size。

    size这个变量有两层含义: ①:元素的个数,也就是集合的长度 ②:下一个元素的存入位置

  2. 添加元素,添加完毕后,size++

扩容时机一:

  1. 当存满时候,会创建一个新的数组,新数组的长度,是原来的1.5倍,也就是长度为15.再把所有的元素,全拷贝到新数组中。如果继续添加数据,这个长度为15的数组也满了,那么下次还会继续扩容,还是1.5倍。

扩容时机二:

  1. 一次性添加多个数据,扩容1.5倍不够,怎么办呀?

    如果一次添加多个元素,1.5倍放不下,那么新创建数组的长度以实际为准。

举个例子: 在一开始,如果默认的长度为10的数组已经装满了,在装满的情况下,我一次性要添加100个数据很显然,10扩容1.5倍,变成15,还是不够,

怎么办?

此时新数组的长度,就以实际情况为准,就是110

具体分析过程可以参见视频讲解。

添加一个元素时的扩容:

添加多个元素时的扩容:

LinkedList源码分析:

底层是双向链表结构

核心步骤如下:

  1. 刚开始创建的时候,底层创建了两个变量:一个记录头结点first,一个记录尾结点last,默认为null

  2. 添加第一个元素时,底层创建一个结点对象,first和last都记录这个结点的地址值

  3. 添加第二个元素时,底层创建一个结点对象,第一个结点会记录第二个结点的地址值,last会记录新结点的地址值

具体分析过程可以参见视频讲解。

12.如何保证ArrayList的线程安全?

(1)使用collentions.synchronizedList()方法为ArrayList加锁

(2)使用Vector,Vector底层与Arraylist相同,但是每个方法都由synchronized修饰,速度很慢

(3)使用juc下的CopyOnWriterArrayList,该类实现了读操作不加锁,写操作时为list创建一个副本,期间其它线程读取的都是原本list,写操作都在副本中进行,写入完成后,再将指针指向副本。

13.ArrayList类常用方法

构造方法

方法名说明
public ArrayList()创建一个空的集合对象

成员方法

方法名说明
public boolean add(要添加的元素)将指定的元素追加到此集合的末尾
public boolean remove(要删除的元素)删除指定元素,返回值表示是否删除成功
public E remove(int index)删除指定索引处的元素,返回被删除的元素
public E set(int index,E element)修改指定索引处的元素,返回被修改的元素
public E get(int index)返回指定索引处的元素
public int size()返回集合中的元素的个数

14.Collections类

Collections常用功能

  • java.utils.Collections是集合工具类,用来对集合进行操作。

    常用方法如下:

  • public static void shuffle(List<?> list):打乱集合顺序。

  • public static <T> void sort(List<T> list):将集合中元素按照默认规则排序。

  • public static <T> void sort(List<T> list,Comparator<? super T> ):将集合中元素按照指定规则排序。

代码演示:

 public class CollectionsDemo {
     public static void main(String[] args) {
         ArrayList<Integer> list = new ArrayList<Integer>();
    
         list.add(100);
         list.add(300);
         list.add(200);
         list.add(50);
         //排序方法 
         Collections.sort(list);
         System.out.println(list);
     }
 }
 结果:
 [50,100, 200, 300]

我们的集合按照默认的自然顺序进行了排列,如果想要指定顺序那该怎么办呢?

Comparator比较器

创建一个学生类,存储到ArrayList集合中完成指定排序操作。

Student 类

 public class Student{
     private String name;
     private int age;
     //构造方法
     //get/set
     //toString
 }

测试类:

 public class Demo {
     public static void main(String[] args) {
         // 创建四个学生对象 存储到集合中
         ArrayList<Student> list = new ArrayList<Student>();
 ​
         list.add(new Student("rose",18));
         list.add(new Student("jack",16));
         list.add(new Student("abc",20));
         Collections.sort(list, new Comparator<Student>() {
           @Override
             public int compare(Student o1, Student o2) {
             return o1.getAge()-o2.getAge();//以学生的年龄升序
          }
         });
 ​
 ​
         for (Student student : list) {
             System.out.println(student);
         }
     }
 }
 Student{name='jack', age=16}
 Student{name='rose', age=18}
 Student{name='abc', age=20}

PS:此文参考了:

2024最强秋招八股文(精简、纯手打)_java八股文-CSDN博客

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值