【学习】java基础

一些面试题

  • 创建对象的四种方式

    • new
    • 反射机制:,调用java.lang.Class 或者 java.lang.reflect.Constructor 类的newInstance()实例方法
    • clone:继承Cloneable接口,Hello h2 = h1.clone();
      public class Hello implements Cloneable{
      	public void sayWorld(){
      		System.out.println("Hello world!");
      	}
      
      	public static void main(String[] args){
      		Hello h1 = new Hello();
      		try{
          		Hello h2 = (Hello)h1.clone();
         			h2.sayWorld();
      		}
      		catch (CloneNotSupportedException e){
          		e.printStackTrace();
      		}
      }
      
    • 序列化:调用java.io.ObjectInputStream 对象的 readObject()方法.
  • 深拷贝 VS 浅拷贝

    • 浅拷贝:仅仅复制引用
    • 深拷贝:把要复制的对象所引用的对象复制了一边,new了一个新的对象
  • 数据类型

    • 基本数据类型:注意
      • 0.1*3 == 0.3 :false,因为小数无法精确表示
      • a = a + b;与a += b;后者会进行隐式自动类型转换(转换成左边的),而前者不会
    • 类型转换
      • 字符串—》基本数据类型:包装类的parseXXX()或valueOf()
      • 基本数据类型–》字符串:与空串连接,String类的valueOf()
  • 拆箱、装箱

    • 装箱:将基本数据类型转换为包装类:Integer i = 10;
    • 拆箱:将包装类对象转换为基本类型
    • 注意:
      在Integer的valueOf(),数值在[-128,127]时返回的已存在对象的引用,超过便会创建一个新的Integer对象
      如Integer i1 = 100;
      Integer i2 = 100;
      Integer i3 = 200;
      Integer i4 = 200;
      i1==i2; true
      i3==i4; false
      对象作为参数时,是值传递

方法重载vs方法重写
- 方法重载:同名不同参,返回值类型不要求
- 方法重写:子类重写父类方法,返回值类型相同

  • 静态嵌套类vs内部类

    • 静态嵌套类:有static关键字的内部类,不依赖于外部类实例而被实例化
    • 内部类:只有外部类实例化才可以实例化
  • 抽象方法不可以被static修饰,不可以是本地方法(本地方法需要实现),不可以被synchronized修饰(synchronized和方法细节有关,但抽象方法无实现)

  • 静态方法不可以在内部调用非静态方法,只能方法静态成员。非静态方法需要被实例化才可以方法

  • 对象克隆(浅拷贝)

    • 实现Cloneable接口并重写clone()
    • 实现Serializable接口,实现对象的序列化和反序列化
  • GC:垃圾回收机制

    • 调用:System.gc();或Runtime.getRuntime().gc();
  • 锁池和等待池
    线程调用对象的wait()后,该线程进入等待池,当该线程调用对象的notifyAll()后,该线程进入锁池,当该线程抢到锁时,该线程处于就绪状态

    • 等待池:等待进入锁池的池子。wait()后进入
    • 锁池:竞争对象锁的池子,抢到对象锁的线程进入就绪状态。notify()后进入

一、日期类

1.1 常见日期类

Date类

  • 包:util.Date
  • 构造器:
    • Date():创建一个Date对象,代表的是系统当前此刻日期时间。
    • Date(long date):将时间毫秒值转换为Date日期对象
  • 时间的两种表示方法
    • yyyy-mm-dd:Date d = new Date();
    • 时间毫秒值:long time = d.getTime();或long time = System.currentTimeMillis()
    • 转换:
  • 常用方法:
名称说明
Date()创建一个Date对象,代表的是系统当前此刻日期时间。
public long getTime()返回从1970年1月1日 00:00:00走到此刻的总的毫秒数
public void setTime(long time)设置日期对象的时间为当前时间毫秒值对应的时间
before(Date date)d1在d2之前,则:d1.after(d2)为true
after(Date date)
  • 案例:请计算出当前时间往后走1小时121秒之后的时间是多少。
    //当前时间
    Date d1 = new Date();
    System.out.println(d1);
    //当前时间的毫秒值
    long time = d1.getTime();//或者long time = System.currentTimeMillis();
    //1小时121秒后
    time += (60*60 + 121) * 1000;
    Date d2 = new Date(time);
    System.out.println(d2);
    

SimpleDateFormat

  • 格式化日期和时间毫秒值
    public class data {
    	public static void main(String[] args) {
        	//1、格式化日期对象
        	//1.1、获取日期对象
        	Date d = new Date();
        	System.out.println(d);
        	//1.2、格式化
        	SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss EEE a");
            String rs = sdf.format(d);
        	System.out.println(rs);
        
        	//2、格式化时间毫秒值
        	long l = System.currentTimeMillis();
        	String rs2 = sdf.format(l);
        	System.out.println(rs2);
    	}
    }
    
  • 解析字符串时间为日期对象
    public static void main(String[] args) throws ParseException {
        //时间:2021年08月06日 11:11:11
    
        String dateStr = "2021年08月06日 11:11:11";
        //形式必须与被解析字符串时间完全一样
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
        Date date = sdf.parse(dateStr);
        
        long time = date.getTime() + (2L*24*60*60 + 14*60*60 + 49*60 + 6) * 1000;
        System.out.println(sdf.format(time));
    
    }
    

Calendar

  • 概述
    • Calendar代表了系统此刻日期对应的日历对象。
      Calendar是一个抽象类,不能直接创建对象。
      calendar是可变日期对象,一旦修改后其对象本身表示的时间将产生变化。
  • 案例
    public static void main(String[] args) {
        // 1、拿到系统此刻日历对象
        Calendar cal = Calendar.getInstance();
        System.out.println(cal);
    
        // 2、获取日历的信息:public int get(int field):取日期中的某个字段信息。
        int year = cal.get(Calendar.YEAR);
        System.out.println(year);
    
        int mm = cal.get(Calendar.MONTH) + 1;
        System.out.println(mm);
    
        int days = cal.get(Calendar.DAY_OF_YEAR) ;
        System.out.println(days);
    
        // 3、public void set(int field,int value):修改日历的某个字段信息。不适合修改
        // cal.set(Calendar.HOUR , 12);
        // System.out.println(cal);
    
        // 4.public void add(int field,int amount):为某个字段增加/减少指定的值
        // 请问64天后是什么时间
        cal.add(Calendar.DAY_OF_YEAR , 64);
        //59分后是什么时间
        cal.add(Calendar.MINUTE , 59);
    
        //  5.public final Date getTime(): 拿到此刻日期对象。
        Date d = cal.getTime();
        System.out.println(d);
    
        //  6.public long getTimeInMillis(): 拿到此刻时间毫秒值
        long time = cal.getTimeInMillis();
        System.out.println(time);
    
    }
    

1.2 JDK8新增日期类

  • 所在包:java.time包
  • 类:(新API的类型几乎全部是不变类型(和String的使用类似))
    LocalDate:年月日
    LocalTime:时分秒
    LocalDateTime:包含了日期及时间。
    Instant:代表的是时间戳。
    DateTimeFormatter 用于做时间的格式化和解析的
    Duration:用于计算两个“时间”间隔
    Period:用于计算两个“日期”间隔

LocalDate、LocalTime、LocalDateTime

  • 概述
    • 分别表示日期,时间,日期时间对象,他们的类的实例是不可变的对象
    • 构建对象
      在这里插入图片描述
    • api
      时间信息api在这里插入图片描述
      LocalDateTime的转换API
      在这里插入图片描述
      修改api(这些方法返回的是一个新的实例引用,因为LocalDateTime 、LocalDate 、LocalTime 都是不可变的)
      在这里插入图片描述

Instant时间戳

  • 概述
    • 时间戳:包含日期和时间,与java.util.Date很类似,事实上Instant就是类似JDK8 以前的Date。
    • 构造:Instant类由一个静态的工厂方法now()可以返回当前时间戳。
      在这里插入图片描述

DateTimeFormatter

Duration/Period

ChronoUnit

二、包装类

  • 包装类:就是8种基本数据类型对应的引用类型
    在这里插入图片描述
  • 作用:
    基本数据类型对应的引用类型,实现了一切皆对象。
    后期集合和泛型不支持基本类型,只能使用包装类
  • 功能:
    1、自动装箱:基本类型的数据和变量可以直接赋值给包装类型的变量。
    2、自动拆箱:包装类型的变量可以直接赋值给基本数据类型的变量
    3、包装类的变量的默认值可以是null,容错率更高:Integer i = null
    4、可以把基本类型的数据转换成字符串类型(用处不大,可以使用+空字符串""转换成字符串)
    5、可以把字符串类型的数值转换成真实的数据类型(⭐⭐)
    在这里插入图片描述

2.1 泛型

  1. 概述
    1. 含义:是JDK5中引入的特性,可以在编译阶段约束操作的数据类型,并进行检查
    2. 格式:<数据类型>; 注意:泛型只能支持引用数据类型。
    3. 位置:
      类后面:泛型类
      方法声明后:泛型方法
      接口后面:泛型接口
  2. 好处:
    统一数据类型。
    将运行时的问题提前到了编译时,避免了强转可能出现的异常
  3. 泛型类:
    格式:public class MyArrayList<T> {}
    核心思想:把出现该泛型变量的地方替换成指定真实数据类型变量
    作用:指定数据类型,类似集合作用
  4. 泛型方法
    格式: public <T> void show(T t) { };
    核心思想:
    作用:方法中可以接收一切实际类型的参数,方法更具备通用性
  5. 泛型接口
    格式:public interface Data<E>{};
    作用:泛型接口可以让实现类选择当前功能需要操作的数据类型
  6. 泛型通配符
    1. ?:代表一切类型
    2. 泛型上限:
      ? extends Car: ?必须是Car或者其子类
    3. 泛型下限:
      ? super Car : ?必须是Car或者其父类

2.2 正则表达式

正则表达式:用一些规定的字符来制定规则,并用来校验数据格式的合法性。

  • 规则:
    • 字符类
      [abc] 只能是a, b, 或c
      [^abc] 除了a, b, c之外的任何字符
      [a-zA-Z] a到z A到Z,包括(范围)
      [a-d[m-p]] a到d,或m通过p:([a-dm-p]联合)
      [a-z&&[def]] d, e, 或f(交集)
      [a-z&&[^bc]] a到z,除了b和c:([ad-z]减法)
      [a-z&&[^m-p]] a到z,除了m到p:([a-lq-z]减法)
    • 预定义字符类
      . 任何字符
      \d 一个数字: [0-9]
      \D 非数字: [^0-9]
      \s 一个空白字符: [ \t\n\x0B\f\r]
      \S 非空白字符: [^\s]
      \w [a-zA-Z_0-9] 英文、数字、下划线
      \W [^\w] 一个非单词字符
    • 贪婪的量词(配合匹配多个字符)
      X? ,X 出现一次或根本不
      X* ,X出现零次或多次
      X+ ,X出现 一次或多次
      X {n} ,X出现正好n次
      X {n, } ,X出现至少n次
      X {n,m}, X出现至少n但不超过m次
  • 案例:
    • 校验手机号码:phone.matches(“1[3-9]\d{9}”)
      第一位只能为1、第二位可以在3-9,后面随便
    • 校验邮箱:email.matches(“\w{1,30}@[a-zA-Z0-9]{2,20}(\.[a-zA-Z0-9]{2,20}){1,2}”)
      \w{1,30}:1-30个字符,如1787050201
      [a-zA-Z0-9]{2,20}:域名,如qq
      \.:表示"."
      (\.[a-zA-Z0-9]{2,20}){1,2}:会出现两级,如com.cn
    • 校验电话号码:tel.matches(“0\d{2,6}-?\d{5,20}”)
    • 校验金额
  • 正则表达式在字符串方法的使用
    • split()和replace()
      public static void main(String[] args) {
      	String names = "小路dhdfhdf342蓉儿43fdffdfbjdfaf小何";
      	//split
      	String[] arrs = names.split("\\w+");
      	for (int i = 0; i < arrs.length; i++) {
          	System.out.println(arrs[i]);
      	}
      	//replace
      	String names2 = names.replaceAll("\\w+", "  ");
      	System.out.println(names2);
      }
      
  • 爬取信息
    在这里插入图片描述

2.3 Lambda表达式

  • 作用:简化函数式接口的匿名内部类的代码写法。
    函数式接口:只有一个抽象方法的接口(可为接口添加@FunctionalInterface)

  • 格式
    在这里插入图片描述

  • 案例:

    • 案例1:
      在这里插入图片描述
    • 案例2:
      在这里插入图片描述
      在这里插入图片描述
  • 省略写法
    1、参数类型可以省略不写。
    2、如果只有一个参数,参数类型可以省略,同时()也可以省略。
    3、如果Lambda表达式的方法体代码只有一行代码。可以省略大括号不写,同时要省略分号!
    4、如果Lambda表达式的方法体代码只有一行代码。可以省略大括号不写。此时,如果这行代码是return语句,必须省略return不写,同时也必须省略";"不写

Arrays.sort(ages1, ( o1, o2) -> o2 - o1);//降序

2.4 Arrays类

数组

  • 定义:String[] arr = {“hello”, “array”};
    • 静态初始化数组
      • 格式:数据类型[] 数组名 = new 数据类型[]{元素1, 元素2, 元素3};
        在这里插入图片描述
      • 注意:
        数据类型[] 数组名 ,也可以写成:数据类型 数组名[]
        数组一经创建,长度类型就固定
    • 动态初始化创建数组
      • 格式:数据类型[] 数组名 = new 数据类型[长度];

数组工具类Arrays

  • 数组操作工具类,专门用于操作数组元素的
  • 常用api
    在这里插入图片描述
    案例:
    public static void main(String[] args) {
        // 目标:学会使用Arrays类的常用API ,并理解其原理
        int[] arr = {10, 2, 55, 23, 24, 100};
        System.out.println(arr);
    
        // 1、返回数组内容的 toString(数组)
    	//  String rs = Arrays.toString(arr);
    	//  System.out.println(rs);
    
        System.out.println(Arrays.toString(arr));
    
        // 2、排序的API(默认自动对数组元素进行升序排序)
        Arrays.sort(arr);
        System.out.println(Arrays.toString(arr));
    
        // 3、二分搜索技术(前提数组必须排好序才支持,否则出bug)
        int index = Arrays.binarySearch(arr, 55);
        System.out.println(index);
    
        // 返回不存在元素的规律: - (应该插入的位置索引 + 1)
        int index2 = Arrays.binarySearch(arr, 22);
        System.out.println(index2);
    
    
        // 注意:数组如果么有排好序,可能会找不到存在的元素,从而出现bug!!
        int[] arr2 = {12, 36, 34, 25 , 13,  24,  234, 100};
        System.out.println(Arrays.binarySearch(arr2 , 36));
    }
    
    • Comparator比较器
      自定义排序规则

2.5 字符串类

不可变字符串String

  • String:String类定义的变量可以用于存储字符串,被称为不可变字符串类型,它的对象在创建后不能被更改。

  • 特点:

    • 为final类,不可被继承
  • 原理:
    String变量每次的修改其实都是产生并指向了新的字符串对象。
    原来的字符串对象都是没有改变的,所以称不可变字符串。

  • 对象的创建

    • 法一:String name = “传智教育”;(存在字符串常量池中,相同值只有一份)
    • 法二:String name = new (“船只教育”)构造器创建
      name:在栈中
      对象1:new出来的对象在堆上
      对象2:船只教育:字符串在方法区的常量池
      在这里插入图片描述
      在这里插入图片描述
  • API在这里插入图片描述

    • 字符串内容比较equals
    • length:
      数组有length属性,没有length方法
      String没有length属性,有length方法

可变字符串(StringBuffer、StringBuilder)

  • 区别:StringBuilder单线程,方法没有被synchronized修饰,效率高
  • 使用:StringBuffer、StringBuilder的reverse方法

三、 集合

在这里插入图片描述

  • 集合vs数组:
    数组:长度固定,类型固定。可以存储基本类型和引用类型
    集合:长度不固定,类型不固定,只能存储引用类型
  • ArrayList和Vector、LinkedList
    • ArrayList:基于数组,不安全,但是可以通过工具类Cellections的synchronizedList方法将其线程安全化
    • Vector:基于数组,线程安全(sychronized),性能查
    • LinkedList:基于链表,不安全,可线程安全化
  • HashMap和HashTable区别
    HashMap:线程不安全
    HashTable:线程安全,加了synchronized锁
    ConcurrentHashMap:线程安全,使用了分段锁,不对整个数据进行锁定

3.1 Collection(单列)

1. collection概述

  • 存储内容:元素对象的地址
  • 特点:长度可变,类型可变
  • 实现:不直接实现,使用子接口实现:Collection<String> c = new ArrayList<String>();
  • API:
    在这里插入图片描述
  • 遍历:
    • 迭代器iterator
      • 适用范围:collection专业遍历方式

      • api:
        在这里插入图片描述

      • 使用步骤

        //Iterator<E> iterator():返回此集合中元素的迭代器,通过集合的iterator()方法得到
        Iterator<String> it = c.iterator();
        //用while循环改进元素的判断和获取
        while (it.hasNext()) {
        	String s = it.next();
        	System.out.println(s);
        }
        
      • foreach/增强for循环

        • 适用范围:集合和数组
        • 特点:不会影响集合中的元素
        • 使用
          在这里插入图片描述
      • lambda表达式
        在这里插入图片描述
        在这里插入图片描述

2. List接口

  1. 介绍

    1. list:序列(有序集合)
    2. 特点:有序 可重复 有索引
  2. 特有api
    在这里插入图片描述

  3. 底层:ArrayList是数组,LinkList是双向链表

  4. 遍历方式:a、迭代器 b、foreach c、lambda d、for循环(因为list有索引)

ArrayList实现类

底层:数组 超过大小会扩容
特点:查询快、增删慢
特有API

LinkedList实现类

底层:双向链表
特点:查询慢、增删快
特有api:
在这里插入图片描述

3. Set接口

  • 特点:无序(存取顺序无序) 不重复 无索引
  • 遍历方式:迭代器、增强for循环
  • 基本使用:Set<String> set = new HashSet<String>();
  • Set实现类:
    HashSet : 无序、不重复、无索引。
    LinkedHashSet:有序、不重复、无索引。
    TreeSet:排序、不重复、无索引。
哈希表
  1. 底层:哈希表(数组+链表)
    1. 哈希值: 根据对象的地址以某种算法得到的数值
      特点:同一对象的哈希值是相同的;默认情况下,不同对象的哈希值是不同的。
    2. 哈希表:
      JDK8之前的,哈希表:底层使用数组+链表组成
      JDK8开始后,哈希表:底层采用数组+链表+红黑树组成。
  2. 哈希表的创建流程
    a、创建一个默认长度16的数组名为table
    b、 根据哈希算法hashCode(元素哈希值与数组长度求余)得到位置
    c、判断当前位置是否为null,如果是null直接存入,如果位置不为null,表示有元素, 则调用equals方法比较属性值,如果一样,则不存,如果不一样,则存入数组,即挂在老元素下面。
    d、当数组存满到16*0.75=12时,就自动扩容,每次扩容原先的两倍
    e、注意:jdk1.8后,当数组的链表长度超过8后,会自动转换成红黑树(优化)
  3. 哈希表的去重:
    1. 默认去重:先判断地址的哈希值,再判断内容的equals方法
    2. 要想实现Set集合去除重复内容的Student对象,就必须重写Student类的hashCode()和equals方法(可以自动重写)
HashSet
  1. HashSet
    3. 案例
    	//可以去重完全相同的Student对象
    	public class StudentDemo {
    	    public static void main(String[] args) {
    	        Set<Student> students = new HashSet<>();
    	        Student student1 = new Student("小米", 20, "女");
    	        Student student2 = new Student("小米", 20, "女");
    	        Student student3 = new Student("小麦", 20, "女");
    	        students.add(student1);
    	        students.add(student2);
    	        students.add(student3);
    
    	        System.out.println(students);
    	    }
    	}
    
    	public class Student {
    	    private String name;
    	    private int age;
    	    private String sex;
    
    	    @Override
    	    public String toString() {
    	        return "Student{" +
    	                "name='" + name + '\'' +
    	                ", age=" + age +
    	                ", sex='" + sex + '\'' +
    	               '}';
    	    }
    		//重写方法
    	    @Override
    	    public boolean equals(Object o) {
    	        if (this == o) return true;
    	        if (o == null || getClass() != o.getClass()) return false;
    	        Student student = (Student) o;
    	        return age == student.age &&
    	                Objects.equals(name, student.name) &&
    	                Objects.equals(sex, student.sex);
    	    }
    		//重写方法
    	    @Override
    		public int hashCode() {
        		return Objects.hash(name, age, sex);
    		}
    	}
    
LinkedHashSet:有序

特点:有序(存储和取出顺序一致)、不重复、无索引
底层:哈希表加双向链表。底层是哈希表,每个元素额外多了双向链表记录添加顺序

TreeSet实现类:有序
  1. 特点:不重复、有序(默认升序)、无索引
    重写hashCode和equals后相同的Student对象还会重复
  2. 底层:红黑树的数据结构
  3. 默认排序规则(默认升序):
    对于数值类型:Integer , Double,官方默认按照大小进行升序排序。
    对于字符串类型:默认按照首字符的编号升序排序。
    对于自定义类型如Student对象,需要制定排序规则,否则程序出错
  4. 自定义排序规则
    1. 方式一
      让自定义的类(如学生类)实现Comparable接口重写里面的compareTo方法来定制比较规则。
    2. 方式二(两种方式,方式二优先级大一点)
      TreeSet集合有参数构造器,可以设置Comparator接口对应的比较器对象,来定制比较规则
    3. 比较规则:
      a、如果TreeSet集合存储的对象有实现比较规则,集合也自带比较器,默认使用集合自带的比较器排序
      b、如果认为第一个元素等于第二个元素返回0即可,此时Treeset集合只会保留一个元素,认为两者重复;
  5. 自定义比较规则案例(根据Student类的age排序,也可设置其他类型)
    1. 实现比较器接口,重写比较方法
      public class StudentDemo {
          public static void main(String[] args) {
              TreeSet<Student> students = new TreeSet<>();
              Student student1 = new Student("小米", 23, "女");
              Student student2 = new Student("小米", 21, "女");
              Student student3 = new Student("小麦", 23, "女");
      
              students.add(student1);
              students.add(student2);
              students.add(student3);
      
              System.out.println(students);
          }   
      }
      
      class Student implements Comparable<Student>{
      	private String name;
          private int age;
          private String sex;
          @Override
          public String toString() {
              return "Student{" +
                      "name='" + name + '\'' +
                     ", age=" + age +
                     ", sex='" + sex + '\'' +
                     '}';
          }
          public Student(String name, int age, String sex) {
              this.name = name;
              this.age = age;
              this.sex = sex;
          }
          //自定义比较规则
          @Override
          public int compareTo(Student o) {
              //按照年龄比较
             //return this.age - o.age;//去重年龄一样的
             return this.age - o.age >= 0?1:-1;//保留年龄一样的
         }
      }
      
    2. 方式二,集合自带比较器规则
      public class StudentDemo {
      	public static void main(String[] args) {
      		Student student1 = new Student("小米", 23, "女");
      		Student student2 = new Student("小米", 23, "女");
      		Student student3 = new Student("小麦", 13, "女");
      	
      		//简写:Set<Student> students2 = new TreeSet<>((o1, o2) -> o2.getAge() - o1.getAge());
              Set<Student> students2 = new TreeSet<>(new Comparator<Student>() {
                 @Override
                  public int compare(Student o1, Student o2) {
                      //return o1.getAge() - o2.getAge();//升序
                     return o2.getAge() - o1.getAge();//降序
                  }
              });
      
              students2.add(student1);
              students2.add(student2);
              students2.add(student3);
              System.out.println(students2);
          }
      }
      

在这里插入图片描述

4. Collections集合工具类

  • 可变参数
    • 含义:形参可接收多个数据
    • 格式:数据类型…参数名称,如:int…nums
    • 作用:传参灵活,可多可无也可数组
    • 条件:a、一个形参列表中,只能有一个可变形参;b、且位置在形参列表最后
    • 实例:
      public static void main(String[] args) {
      
      	int[] arr = {4,5,6,7};
          sum();
          sum(1);
          sum(2,3);
          sum(arr);
      }
      
      public static void sum(int...nums) {
         //nums在方法内部为数组
        System.out.println(Arrays.toString(nums));
      }
      
  1. 包:java.utils.Collections,集合工具类
    重构键:shift+f6
  2. 作用:Collections并不属于集合,是用来操作集合的工具类。
  3. api:
    方法说明
    public static boolean addAll(Collection<? super T> c, T… elements)给集合对象批量添加元素
    public static void shuffle(List<?> list)打乱List集合元素的顺序
    public static void sort(List list)将集合中元素按照默认规则排序(不可以直接对自定义类型的List集合排序,除非自定义类型实现了比较规则Comparable接口)
    public static void sort(List list,Comparator<? super T> c)将集合中元素按照指定规则排序
    • sort()排序:
      使用默认排序规则。自定义类型需要继承Comparable接口,没有则需要添加Comparator比较器
    • 线程安全化:
  4. 案例:
    public static void main(String[] args) {
    
        List<String> people = new ArrayList<>();
    
        //1、addAll()
        Collections.addAll(people,"小红", "小黄", "校长");
        System.out.println("addAll------>" + people);
        //2、shuffer
        Collections.shuffle(people);
        System.out.println("shuffer----->" + people);
        //3、sort(自定义类型需要继承Comparable接口重写比较方法,否则报错)
        Collections.sort(people);
        System.out.println("sort----->" + people);
    
        List<Student> students = new ArrayList<>();
        Student student1 = new Student("abc", 20, "女");
        Student student2 = new Student("abc", 10, "女");
        Student student3 = new Student("abc", 100, "女");
        Collections.addAll(students, student1, student2, student3);
    
        //4、sort方式2
        Collections.sort(students, new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return o1.getAge() - o2.getAge();
            }
        });
        System.out.println("students---sort2-->" + students);
    
    }
    

5. 并发修改异常问题

迭代器:遍历集合但是用迭代器自己的删除方法操作可以解决
for循环遍历:有索引、当删除值后,将索引减1即可恢复

3.2 map(键值对)

1. 概述

  1. Map :双列集合,每个元素包含两个数据,key=value形式

  2. 底层:哈希表

  3. 完整格式:{key1=value1 , key2=value2 , key3=value3 , …}
    collection集合:[元素1, 元素2, 元素3…]

  4. 特点
    由键决定的
    无序,不重复的,无索引的,值不做要求(可以重复)
    重复的键对应的值会覆盖前面重复键的值。
    键值对都可以为null

  5. 实现类
    a、HashMap:元素按照键是无序,不重复,无索引,值不做要求。(与Map体系一致)
    b、LinkedHashMap:元素按照键是有序,不重复,无索引,值不做要求。
    c、TreeMap:元素按照建是排序,不重复,无索引的,值不做要求。

  6. 声明:Map<String, Integer> maps = new HashMap<>();

  7. Map接口的api
    在这里插入图片描述
    案例:

    public static void main(String[] args) {
        //1、声明
        HashMap<String, Integer> map = new HashMap<>();
    
        //2、添加数据
        map.put("小红", 10);
        map.put("小米", 18);
        map.put("小方", 38);
        map.put("小狗", 1);
    
        //3、清空
        //map.clear();
    
        //4、判空
        System.out.println("map为空:" + map.isEmpty());
    
        //5、 根据key删除元素,返回值为key对应的value
        System.out.println("小狗的年龄" + map.remove("小狗"));
    
        //6、判断是否含有key
        System.out.println("是否含有‘小米’:" + map.containsKey("小米"));
    
        //7、判断是否含有value
        System.out.println("是否含有18岁的:" + map.containsValue(18));
    
    	//8、根据key获取对应的value
    	System.out.println("小方的年龄" + map.get("小方"));
    
        //8、获取全部的key集合:返回值是set是因为key无序不重复无索引
        Set<String> keys = map.keySet();
        System.out.println("所有的key:" + keys);
    
        //9.获取全部的value集合:返回值是collection是因为collection可重复
        Collection<Integer> values = map.values();
        System.out.println("所有的value:" + values);
    
        //10、获取map的大小
        System.out.println("map的大小为:" + map.size());
    
        //11、合并其他的map,map1.putAll(map2):将map2拷贝到map1中
        
    
        System.out.println(map);
    
    }
    
  8. 遍历方式
    方式一:键找值的方式遍历:先获取Map集合全部的键,再根据遍历键找值。
    方式二:键值对的方式遍历,把“键值对“看成一个整体,难度较大。
    方式三:JDK 1.8开始之后的新技术:Lambda表达式。

2. 遍历方式

键找值
  1. 步骤:键找值的方式遍历:先获取Map集合全部的键,再根据遍历键找值。
  2. 代码
    	HashMap<String, Integer> map = new HashMap<>();
    
        map.put("小红", 10);
        map.put("小米", 18);
        map.put("小方", 38);
        map.put("小狗", 1);
    
        //1、获取全部的key
        Set<String> keys = map.keySet();
        //2、根据key,依次获取value
        for (String key:keys) {
            System.out.println(map.get(key));
        }
    
键值对
  1. 步骤:把“键值对“看成一个整体。a、先把Map集合转换成Set集合,Set集合中每个元素都是键值对实体类型了。b、遍历Set集合,然后提取键以及提取值。
  2. 代码
    public static void main(String[] args) {
        HashMap<String, Integer> map = new HashMap<>();
    
        map.put("小红", 10);
        map.put("小米", 18);
        map.put("小方", 38);
        map.put("小狗", 1);
    
         //1、将map集合转换为set集合
        Set<Map.Entry<String, Integer>> entries = map.entrySet();
        //2、根据key,依次获取value
        for (Map.Entry<String, Integer> entry:entries) {
            System.out.println("key:" + entry.getKey());
            System.out.println("value:" + entry.getValue());
            System.out.println("key=value:" + entry);
        }
    
    }
    
    在这里插入图片描述
lambda
  • 代码:
    map.forEach(new BiConsumer<String, Integer>() {
        @Override
        public void accept(String s, Integer integer) {
            System.out.println(s + "=" + integer);
        }
    });
    //简写:map.forEach((s, integer) -> System.out.println(s + "=" + integer));
    

3. 实现类

HashMap⭐

特点:无序、不重复(依赖hashCode和equals)、无所引
底层:哈希表
存储:当key是自定义对象时需要重写hashCode和equals

linkedHashMap

特点:有序(双链表)、不重复、无索引
底层:哈希表,增加了双链表记录顺序

HashTable

特点:有序

TreeMap

原理:和TreeSet一样
特点:有序、不重复、无所引
底层:哈希表+红黑树(它是treeSet的底层)
要求:必须为key实现排序方法,可以默认排序,也可以自定义排序
排序规则:1、类实现Comparable接口,2、TreeMap集合有参数构造器,可以new一个Comparator接口对应的比较器对象,来定制比较规则

4. 嵌套综合案例

在这里插入图片描述
代码

    public static void main(String[] args) {
        //1 利用map集合记录学生选择情况(姓名:选择)
        Map<String, List<String>> maps = new HashMap<>();
        
        List<String> selects1 = new ArrayList<>();
        Collections.addAll( selects1, "B","A");
        maps.put("小红", selects1);
        List<String> selects2 = new ArrayList<>();
        Collections.addAll( selects2, "C","A");
        maps.put("小黑", selects2);
        List<String> selects3 = new ArrayList<>();
        Collections.addAll( selects3, "D","A","B");
        maps.put("小白", selects3);

        //2 利用map集合记录不同地点被选择的次数
        Map<String, Integer> counts = new HashMap<>();
        
        //3 插入数据
        //3.1 提取所有的value,values = [[B,A], [C, A],[D,A,B]]
        Collection<List<String>> values = maps.values();
        //3.2 遍历values,为counts添加数据
        for (List<String> value : values) {
            //value = [B,A]
            for (String s : value) {
                //counts中是否有s,有则存入s=get(s)+1,没有则存s=1
                if (counts.containsKey(s))
                    counts.put(s,counts.get(s)+1);
                else
                    counts.put(s,1);
            }
        }
        System.out.println(counts);
    }

四、多线程

线程安全:vector、stack、hashtable

4.1 线程Thread

1 概述

  1. 线程(thread):一个程序内部的一条执行路径
    包:java.lang.Thread 类
    name属性:默认为线程1、线程2…
  2. 分类
    1. 单线程程序:单独的执行路径,如main()
    2. 多线程:从软硬件上实现多条执行流程的技术,如消息通信、淘宝
  3. 构造器
    public Thread(String name)可以为当前线程指定名称
    public Thread(Runnable target)把Runnable对象交给线程对象
    public Thread(Runnable target ,String name )把Runnable对象交给线程对象,并指定线程名
  4. 使用:
    Thread t1 = new MyThread("线程1");
    //
    Runnable target = new MyRunnable();
    Thread t2 = new MyThread(target, "线程1");
    

2. 线程Thread的API

方法作用
run线程任务方法
start启动线程方法
getName获取线程名
setName设置线程名
currentThread获取当前线程对象
  • run VS start
    线程启动时,不可以直接调用run(),因为这是普通调用。start才是真正启动线程的方法
  • setName/getName
    • 方法一:t.setName();

      	Thread t1 = new MyThread();
      	//设置线程名
      	t1.setName("线程1");
      	t1.start();
      
      	//获取当前线程名
      	prtintf(Thread.currentThread().getName()):
      
    • 方法二:构造器设置

      	//为MyThread添加一个含参构造器
      	//声明并复制
      	Thread t2 = new MyThread("线程2");
      
  • currentThread
    哪个线程执行中调用的,就会得到哪个线程对象。

3. 线程创建

方法一:直接使用线程类继承Thread类
  1. 步骤:
    1、定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
    2、创建MyThread类的对象
    3、调用线程对象的start()方法启动线程(启动后还是执行run方法的)

  2. 优缺
    优点:简单
    缺点:线程类已经继承Thread,无法继承其他类,不利于扩展。

  3. 注意

    1. 为什么不直接调用run,而是start启动线程?
      因为直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。只有调用start方法才是启动一个新的线程执行
    2. 把主线程任务放在子线程之前
      这样主线程一直是先跑完的,相当于是一个单线程的效果了
    3. 把主线程任务放在子线程之后
      start后,t线程和主线程同时启动。主子线程随机顺序执行
  4. 实现:

    		public class ThreadPoolDemo1 {
    		    public static void main(String[] args) throws ExecutionException, InterruptedException {
    		        //3、获取Thread对象
    		        Thread t = new MyThread();
    		        //4、启动线程
    		        t.start();
        
    		        //主线程
    		        for (int i = 0; i < 5; i++) {
    		            System.out.println("主线程执行:" + i);
    		        }
    
    		    }
    		}
    		
    		//1、定义线程类
    		class MyThread extends Thread{
    		    //2、重写run方法,run方法为该线程要干什么事
    		    @Override
    		    public void run(){
    		        for (int i = 0; i < 5; i++) {
    		            System.out.println("子线程执行:" + i);
    		        }
    		    }
    		}
    
     ![在这里插入图片描述](https://img-blog.csdnimg.cn/97b18df826db48dbb79bd4aefebff83a.png)
    
方式二:使用任务类实现Runnable接口
  1. 步骤
    1、定义一个任务类MyRunnable实现Runnable接口,重写run()方法
    2、创建MyRunnable任务对象
    3、创建Thread线程对象,将MyRunnable任务对象交给Thread处理。
    4、调用线程对象的start()方法启动线程

  2. 优缺:
    优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
    缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的。

  3. 实现

    public class Demo1 {
    	public static void main(String[] args) throws ExecutionException, InterruptedException {
        	//3、获取任务对象
        	Runnable target = new MyRunnable();
        	//4、将任务对象交给线程
        	Thread t = new Thread(target);
        	//5、启动线程
        	t.start();
    
        	//主线程
        	for (int i = 0; i < 10; i++) {
            	System.out.println("主线程执行:" + i);
        	}
    
    	}
    }
    
    //1、定义一个线程任务类,实现Runnable接口
    class MyRunnable implements Runnable{
    //2、重写run方法,即所执行任务
    	@Override
    	public void run() {
        	for (int i = 0;i < 10; i++){
            	//Thread.currentThread().getName():获取当前线程的名字,默认为数字
            	System.out.println(Thread.currentThread().getName() + "输出了" + i);
        	}
    
    	}
    }
    
  4. 简化
    可以创建Runnable的匿名内部类对象。
    交给Thread处理。
    调用线程对象的start()启动线程。

        //简化1:
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("儿子线程执行:" + i);
                }
            }
        });
        t.start();
        
        //简化2
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("儿子线程执行:" + i);
                }
            }
        }).start();
        
        //简化3
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println("儿子线程执行:" + i);
            }
        }).start();
    
方式三:任务类实现Callable接口(jdk5)
  1. 前两种弊端
    run()无法直接返回结果,只适合不需要返回结果的业务场景
  2. 优点:可以得到线程执行的结果,扩展性强
  3. 步骤
    1. 得到任务对象
      1. 定义Callable任务类
        实现Callable接口(添加返回值的泛型)、重写call方法(call方法即以前没有返回值的run(),区别是call有返回值)
      2. 用FutureTask把Callable对象封装成线程任务对象。
        FutureTask<String> f1 = new FutureTask<>(call);
        FutureTask的get()可得到任务(线程)返回结果
    2. 把线程任务对象交给Thread处理。
    3. 调用Thread的start方法启动线程,执行任务
    4. 线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。
  4. 实现
    public class ThreadPoolDemo1 {
    	public static void main(String[] args) throws ExecutionException, InterruptedException {
        	//1、获取Callable对象
        	Callable call = new MyCallable(10);
        	//2、将Callable任务交给线程Thread
        	//2.1、先将Callable任务交给FutureTask(底层是Runnable),FutureTask的get()可得到任务结果
        	FutureTask<String> f1 = new FutureTask<>(call);
        	//2.2、将FutureTask交给线程Thread
        	Thread thread = new Thread(f1);
        	//3、启动线程
        	thread.start();
    
    
        	Callable call2 = new MyCallable(20);
        	FutureTask<String> f2 = new FutureTask<>(call2);
        	Thread thread2 = new Thread(f2);
        	thread2.start();
    
        	//4、获得线程结果
        	try{
            	//如果f1没有执行完毕,这里的代码会等待,直到线程1跑完才执行f1.get()
            	System.out.println(f1.get());
        	}catch (Exception e){
            
            }
        	try{
            	//如果f1没有执行完毕,这里的代码会等待,直到线程2跑完才执行f2.get()
            	System.out.println(f2.get());
        	}catch (Exception e){
    
        	}
        
        	//主线程
        	for (int i = 0; i < 10; i++) {
            	System.out.println("主线程执行:" + i);
        	}
    
    	}
    }
    
    //定义一个Callable任务类,实现接口,声明任务执行完毕后返回结果的数据类型,重写call方法,
    class MyCallable implements Callable<String> {
    	private int n;
    
    	public MyCallable(int n) { this.n = n;
    	}
    
    	@Override
    	public String call() throws Exception {
        	int sum = 0;
        	for (int i = 1; i <= n; i++) {
            	sum += i;
        	}
        	return Thread.currentThread().getName() + "执行1-" + n + "结果是:" + sum;
    	}
    }
    
    在这里插入图片描述

4. 线程优先级

  1. 线程调度
    1. 分类:
      1. 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
      2. 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
    2. Java的机制:抢占式。(随机性)
      CPU某一时刻只能执行一条指令,某个线程得到CPU时间片即使用权才可以执行指令。线程抢到使用权是随机的,所以多线程程序具有随机性
  2. 线程优先级
    • 优先级范围:1-10
      最高优先级:Thread.MAX_PRIORITY //10
      最低优先级:Thread.MIN_PRIORITY //1
      默认优先级:Thread.NORM_PRIORITY //5

    • api

      方法说明
      final int getPriority()返回此线程的优先级
      final void setPriority(int newPriority)更改此线程的优先级

5. 线程控制

  • api
    • 线程操作对象的API
      方法作用
      wait()使线程阻塞,使线程释放所持有对象的锁
      notify()唤醒处于等待的线程,jvm决定唤醒哪个线程,与优先级无关
      notifyAll()唤醒所有处于等待的线程,然后让他们竞争,最终获得锁的线程进入就绪
    • 线程本身状态的API
      方法作用名称
      static void sleep(long millis)使线程睡眠,调用此方法需要处理异常休眠
      public static void yield()当前线程主动放弃时间片,返回就绪放弃(让渡)
      void join()等待这个线程死亡合并(插队)
      void setDaemon(boolean on)将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出守护线程
      stop(不推荐)强制线程终止,相当于电脑断电
      interrupt()打断线程
    • 实现线程停止:
      • 法一:stop()(不推荐)
      • 法二:interrupt:系统对现场定义的标识,通过改变该标识的值实现线程停止
        注:当线程正在休眠,通过interrupt终止线程会异常
        使用:主线程 停用 线程1类
        线程1需要添加
        //当该线程被interrupt时,该线程执行break
        if(Thread.interrupted()){
        	break;
        }
        
        主线程
        //停用线程
        t1.interrupt;
        
      • 法三:添加自定义标识
        private
        
  • 代码:
    //sleep
    @Override
    public void run() {
    	for (int i = 0; i < 100; i++) {
    		System.out.println(getName() + ":" + i);
    		try {
    			Thread.sleep(1000);
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    	}
    }
    //join:当康熙这个线程执行结束后,四阿哥和八阿哥才会开始执行
    t1.setName("康熙");
    t2.setName("四阿哥");
    t3.setName("八阿哥");
    t1.start();
    try {
    	t1.join();
    } catch (InterruptedException e) {
    	e.printStackTrace();
    }
    t2.start();
    t3.start();
    
    //setDaemon:当刘备执行结束后,关羽和张飞会立马结束(有一点延迟)
    //关羽张飞执行输出0-99
    t1.setName("关羽");
    t2.setName("张飞");
    //设置主线程为刘备,刘备输出0-9
    Thread.currentThread().setName("刘备");
    //设置守护线程
    t1.setDaemon(true);
    t2.setDaemon(true);
    t1.start();
    t2.start();
    for(int i=0; i<10; i++) {
    	System.out.println(Thread.currentThread().getName()+":"+i);
    }
    
  • sleep VS wait
    • sleep:休眠,Thread的静态方法,调用会将CPU让给其他线程,但是对象锁依然保持,结束后恢复就绪
    • wait:Object类的方法,线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁,进入对象等待池。只有调用对象的notify()或notifyAll()才能唤醒等待池中的线程进入锁池,准备重新竞争资源,当线程重新获得对象的锁后就进入就绪。
  • yield() vs sleep()
    • yeild给其他线程运行机会时考虑优先级,只会给同等优先级或更高优先级线程机会。yeild后就绪。不抛异常
    • sleep不考虑优先级。sleep后阻塞。会抛异常。可移植性高

6. 线程生命周期

在这里插入图片描述

  • 阻塞:
    调用wait()进入等待池
    同步代码块被其他线程执行进入等锁池
    调用sleep()或join()等待休眠或其他线程结束
    io终端

7. 线程死锁

  • 死锁:
    当一个线程持有锁A,等待锁B,另一个线程持有锁B,等待锁A。两个线程都不会释放锁,也无法获取锁,则产生死锁。

8. 线程通信

  1. 线程通信:线程间相互发送数据,线程间共享一个资源即可实现线程通信
  2. 常见形式:
    1、通过共享一个数据的方式实现。
    2、根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做。

4.2 线程同步

问题:线程安全
  1. 线程安全:多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。

  2. 原因:存在多线程并发、同时访问共享资源、存在修改共享资源

  3. 情景

    1. 需求:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时去取钱10万。
    2. 分析:
      ①:需要提供一个账户类,创建一个账户对象代表2个人的共享账户。Account类
      public class Account {
          private String cardId;  //卡号
          private double money; // 余额 关键信息
          
      	//getset构造器
      
          //取钱
          public void drawMoney(double money) {
              // 1、判断是谁来取钱
              String name = Thread.currentThread().getName();
              // 2、判断余额是否足够
              if(this.money >= money){
                  // 2.1 钱够了
      			System.out.println(name+"来取钱,吐出:" + money);
                 // 更新余额
                 this.money -= money;
                 System.out.println(name+"取钱后,余额剩余:" + this.money);
              }else{
                  // 2.2 余额不足
                  System.out.println(name+"来取钱,余额不足!");
              }
          }
      }
      
      ②:需要定义一个线程类,线程类可以处理账户对象。
      public class DrawThread extends Thread{
      
          private Account acc;
          public DrawThread(Account acc, String name){
              //Thread类有name属性,即该线程的名字,将本次线程命名为name
              super(name);
              this.acc = acc;
          }
      
          @Override
          public void run() {
              acc.drawMoney(100000);
          }
      }
      
      ③:创建测试类
      创建2个线程对象,传入同一个账户对象。
      启动2个线程,去同一个账户对象中取钱10万
      public class TestSafeDemp {
      	public static void main(String[] args) {
      		//取钱问题
      		//1 创建一个共享的账户对象
      		Account acc = new Account();
      		//2 创建2个线程对象,操作同一个账户对象
      		new DrawThread(acc,"小明");
      		new DrawThread(acc,"小红");
      	}
      }
      
    3. 问题:
      在这里插入图片描述
  4. 解决:线程同步,即让多个线程实现先后依次访问共享数据,

方法一:同步代码块加锁
  1. 思想:让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
  2. 实现方式:把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来
    synchronized(同步锁对象) {
    	操作共享资源的代码(核心代码)
    }
    
  3. 注意:
    • 当一个线程进入一个对象的synchronized的方法A之后,其他线程无法进入此对象的synchronized方法B
      原因:当有一个进程进入A方法,说明对象锁已被取走,那么其他线程想进入B,需要在等锁池等待对象锁。
  4. 锁对象(即括号内部的)要求:
    1. 规范上:建议使用共享资源作为锁对象。
    *对于实例方法,共享资源建议使用this作为锁对象。
    *对于静态方法,共享资源建议使用字节码(类名.class)对象作为锁对象
    2. 不可以为任意对象:会影响到其他无关线程
  • 案例:

    • 代码:
      为Account的取钱方法的关键代码块添加synchronized锁:选择所选代码,按ctrl+alt+t,
      	//取钱
          public void drawMoney(double money) {
              // 1、判断是谁来取钱
              String name = Thread.currentThread().getName();
              //同步代码块加锁,()里为唯一对象,"person"
              synchronized (this) {
                  // 2、判断余额是否足够
                  if(this.money >= money){
                      // 2.1 钱够了
                      System.out.println(name+"来取钱,吐出:" + money);
                      // 更新余额
                      this.money -= money;
                      System.out.println(name+"取钱后,余额剩余:" + this.money);
                  }else{
                      // 2.2 余额不足
                      System.out.println(name+"来取钱,余额不足!");
                  }
              }
          }
      
方法二:同步方法加锁
  1. 作用:把出现线程安全问题的核心方法给上锁。
  2. 原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
    a、对于实例方法默认使用this作为锁对象。
    b、对于静态方法默认使用类名.class对象作为锁对象。
  3. 格式
    修饰符 synchronized 返回值类型 方法名称(形参列表) {
    	操作共享资源的代码
    }
    
  4. 底层
    同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
    如果核心方法为实例方法,默认使用this作为锁对象
    如果核心方法是静态方法,默认使用类名.class作为锁对象
  5. 代码
    //取钱
    public synchronized void drawMoney(double money) {
        // 0、先获取是谁来取钱,线程的名字就是人名
        String name = Thread.currentThread().getName();
        // 1、判断账户是否够钱
        if(this.money >= money){
            // 2、取钱
            System.out.println(name + "来取钱成功,吐出:" + money);
            // 3、更新余额
            this.money -= money;
            System.out.println(name + "取钱后剩余:" + this.money);
        }else {
            // 4、余额不足
            System.out.println(name +"来取钱,余额不足!");
        }
        
    }
    
方法三:lock锁(jdk5)
  1. lock:lock是接口,不可实例化,采用它的实现类ReentrantLock来构建Lock锁对象。
  2. 使用步骤
    1. 为账户类添加一个私有锁:private final Lock lock = new ReentrantLock();唯一不可替换
    2. 在核心代码块之前上锁:lock.lock();
    3. 核心代码块结束时解锁:lock.unlock();
    4. 优化:当核心代码异常时,无法执行到解锁步骤,则程序崩溃。故需要加核心代码块放在try catch中
  3. 代码:Account类
    private String cardId;  //卡号
    private double money; // 余额 关键信息
    private final Lock lock = new ReentrantLock();
    
    //取钱
    public void drawMoney(double money) {
        // 1、判断是谁来取钱
        String name = Thread.currentThread().getName();
        //上锁
        lock.lock();
        try {
            // 2、判断余额是否足够
            if(this.money >= money){
                // 2.1 钱够了
                System.out.println(name+"来取钱,吐出:" + money);
                // 更新余额
                this.money -= money;
                System.out.println(name+"取钱后,余额剩余:" + this.money);
            }else{
                // 2.2 余额不足
                System.out.println(name+"来取钱,余额不足!");
            }
        } finally {
        	//解锁
            lock.unlock();
        }
    }
    
    
sychronized VS lock
  • sychronized:只能特定情况下(程序正常执行或异常情况jvm自动释放锁)放锁、无法知道进程是否成功获得锁
  • lock:可以自由放锁(要求程序员手动在finally块中释放锁)、可以知道进程有没有成功获得锁

是同步代码块好还是同步方法好一点?
同步代码块锁的范围更小(性能更好),同步方法锁的范围更大(使用较多)

4.3 线程池ExecutorService

1. 概述

  1. 含义:可以复用线程的技术。
    线程池接口:ExecutorService
  2. 场景:如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能。
  3. 原理:
    在这里插入图片描述
  • 获取线程池对象:
    方法一:⭐使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象
    方法二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象

  • 线程池面试题:

    • 临时线程创建时机:新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程
    • 拒绝任务时机:核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝

2. TreadPoolExecutor创建线程池

TreadPoolExecutor介绍
  1. TreadPoolExecutor构造器
    在这里插入图片描述

  2. 线程池api

    方法说明
    void execute(Runnable command)执行任务/命令,没有返回值,一般用来执行 Runnable 任务
    Future submit(Callable task)执行任务,返回未来任务对象获取线程结果,一般拿来执行 Callable 任务
    void shutdown()等全部任务执行完毕后关闭线程池
    List shutdownNow()立刻关闭,停止正在执行的任务,并返回队列中未执行的任务。即使任务未完成,会丢失任务
  3. 使用方法

    1. 创建线程池对象
      ExecutorService pools = new ThreadPoolExecutor(3, 5 , 8 , TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory() , new ThreadPoolExecutor.AbortPolicy());
    2. 为线程池添加任务
    3. 运行程序
  4. 任务增多时

    1. 进入任务队列
      当为任务设置很长的休眠时间时,前三个任务正在执行,再加入第4-8个任务都会进入任务队列等待执行
    2. 临时线程
      当加入第9个任务时,由于任务队列已满,就会开始创建临时线程(最多两个临时线程)
    3. 任务拒绝
      当加入第11个任务时,就会出现任务忙,拒绝任务,即出错
  5. 新任务拒绝策略
    在这里插入图片描述

处理Runnable任务(execute方法)
  1. 定义Runnable任务,MyRunnable.class
    public class MyRunnable implements Runnable{
    
        @Override
        public void run() {
            for (int i = 0;i < 5; i++){
                //Thread.currentThread().getName():获取当前任务的名字,默认为数字
               System.out.println(Thread.currentThread().getName() + "输出了" + i);
            try {
            	//当前三个任务在休眠时,后两个任务不会执行,会进入任务队列
            	System.out.println("正在休眠");
            	Thread.sleep(1000000);
       		} catch (InterruptedException e) {
            	e.printStackTrace();
    	    }
          }
       }
    }
    
  2. 定义线程池
    public class ThreadPoolDemo1 {
        public static void main(String[] args) {
        	//1、获取线程池
            ThreadPoolExecutor pools = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
            //2、为线程池添加5个Runnable任务
            Runnable target = new MyRunnable();
            pools.execute(target);
            pools.execute(target);
            pools.execute(target);
            pools.execute(target);
            pools.execute(target);
    
        }
    }
    
  3. 结果
    在这里插入图片描述
处理Callable任务(submit方法)
  1. 编写Callable类
    /定义一个Callable任务类,实现接口,声明任务执行完毕后返回结果的数据类型,重写call方法,
    public class MyCallable implements Callable<String> {
    	private int n;
    
    	public MyCallable(int n) { this.n = n;}
    
    	@Override
    	public String call() throws Exception {
        	int sum = 0;
        	for (int i = 1; i <= n; i++) {
            	sum += i;
        	}
        	return Thread.currentThread().getName() + "执行1-" + n + "结果是:" + sum;
    	}
    }
    
  2. 编写测试类
    public class ThreadPoolDemo1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1、声明线程池
        ThreadPoolExecutor pools = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS, new ArrayBlockingQueue<>(6), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
    
        //2、把Callable任务交给线程池
        Future<String> f1 = pools.submit(new MyCallable(100));
        Future<String> f2 = pools.submit(new MyCallable(200));
        Future<String> f3 = pools.submit(new MyCallable(300));
        Future<String> f4 = pools.submit(new MyCallable(400));
    
        //3、f1.get():获取任务1的结果
        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());
    }
    }
    

3. Executors工具类创建线程池(不建议)

  1. Executors:线程池的工具类通过调用方法返回不同类型的线程池对象
    Executors的底层:也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的
    缺点:队列可以无限添加任务,但是不会报错,因此在大型并发系统环境中使用Executors如果不注意可能会出现系统风险
  2. api
    方法说明
    public static ExecutorService newCachedThreadPool()线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了一段时间则会被回收掉。
    public static ExecutorService newFixedThreadPool​(int nThreads)创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。
    public static ExecutorService newSingleThreadExecutor ()创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。
    public static ScheduledExecutorService newScheduledThreadPool​(int corePoolSize)创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。
  3. 实例
    • newFixedThreadPool

      public class ThreadPoolDemo1 {
      	public static void main(String[] args) throws ExecutionException, InterruptedException {
      		//1、声明只有3个线程的线程池
      		ExecutorService pools = Executors.newFixedThreadPool(3);
      		//2、添加任务(只有前3个执行完毕后,第四个才可以执行)
      		pools.execute(new MyRunnable());
      		pools.execute(new MyRunnable());
      		pools.execute(new MyRunnable());
      		pools.execute(new MyRunnable());
      
      	}
      }
      

4.4 补充

1. 定时器

  1. 定时器是一种控制任务延时调用,或者周期调用的技术。
  2. 作用:闹钟、定时邮件发送。
  3. 定时器的实现方式
    方式一:Timer
    方式二: ScheduledExecutorService
Timer创建定时器
  1. Timer定时器

  2. api

    方法说明
    public Timer()创建Timer定时器对象
    public void schedule​(TimerTask task, long delay, long period开启一个定时器,按照计划处理TimerTask任务

    TimerTask:底层是Runnable,即任务
    delay:启动后过多长时间开始执行
    period:执行周期时间

  3. 使用:

    public class ThreadPoolDemo1 {
    	public static void main(String[] args) throws ExecutionException, InterruptedException {
        	//1、创建Timer对象
        	Timer timer = new Timer();
        	//2、为Timer添加任务
        	timer.schedule(new TimerTask() {
            	@Override
            	public void run() {
                    System.out.println(Thread.currentThread().getName() + "执行AAA" + new Date());
            	}
        	}, 0, 2000);
        }
    }
    
  4. 缺点:
    1、Timer是单线程,处理多个任务按照顺序执行,存在延时与设置定时器的时间有出入。
    2、可能因为其中的某个任务的异常使Timer线程死掉,从而影响后续任务执行。

ScheduledExecutorService创建定时器
  1. ScheduledExecutorService是:jdk1.5中引入了并发包,目的是为了弥补Timer的缺陷, ScheduledExecutorService内部为线程池。

  2. 底层:基于线程池ExecutorService,某个任务的执行情况不会影响其他定时任务的执行。

  3. api:
    在这里插入图片描述

  4. 使用:

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1、获取ScheduledExecutorService对象(获取ScheduledExecutorService对象基于ExecutorService,故可以使用Executors工具类)
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
        //2、开启定时任务A
        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "执行AAA   ==>" + new Date());
                try {
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },0,2,TimeUnit.SECONDS);
    	//3、开启定时任务B
        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "执行BBB   ==>" + new Date());
            }
        },0,2,TimeUnit.SECONDS);
    
    
    }
    

2. 并发与并行

  1. 条件:正在运行的程序(软件)就是一个独立的进程, 线程是属于进程的,多个线程其实是并发与并行同时进行的
  2. 并发:CPU轮询为系统的每个线程服务。一位老师用一个手掌依次打学生
  3. 并行:CPU同时服务多个线程。一位老师用两个手掌同时多个学生

3. 线程的生命周期

  1. 线程Tread类中定义了6种状态
    在这里插入图片描述
  2. 状态转换:
    在这里插入图片描述

五、包、权限修饰符、关键字、抽象类、接口

5.1 包

  • 语法格式:package 公司域名倒写.技术名称。报名建议全部英文小写,且具备意义
  • 权限:
    1、相同包下的类可以直接访问,不同包下的类必须导包,才可以使用
    2、假如一个类中需要用到不同类,而这个两个类的名称是一样的,那么默认只能导入一个类,另一个类要带包名访问:com.itheima.demo2.Cat c2 = new com.itheima.demo2.Cat();
  • 导包:import 包名.类名;

5.2 权限修饰符

  • 权限修饰符:是用来控制一个成员能够被访问的范围的
  • 修饰范围:成员变量,方法,构造器,内部类
  • 分类:作用范围由小到大(private -> 缺省 -> protected - > public )
    在这里插入图片描述
  • 使用习惯
    1、成员变量一般私有。
    2、方法一般公开。
    3、如果该成员只希望本类访问,使用private修饰。
    4、如果该成员只希望本类,同一个包下的其他类和子类访问,使用protected修饰。

5.3 抽象类和接口

1. 抽象类和抽象方法

  • 抽象类
    • 含义:某个父类知道其所有子类要完成某个功能,但是不同子类完成情况不一样。父类只定义该功能的基本要求,具体实现交给子类。这个类就是抽象类。abstract修饰
    • 声明:修饰符 abstract class 类名{ }
    • 成员:
      构造器:被继承后,子类必须调用抽象类的构造器。
      成员变量
      方法:抽象方法,实例方法
    • 作用:被子类继承,提高代码重复利用
    • 注意
      • 抽象类不可被实例化
        原因:抽象类的抽象方法无法执行
      • 一个类如果继承了抽象类,那么这个类必须重写完抽象类的全部抽象方法,否则这个类也必须定义成抽象类。
      • 抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类
  • 抽象方法
    • 含义:没有方法体,只有方法声明。abstract修饰
    • 格式:修饰符 abstract 返回值类型 方法名称(形参列表);
    • 作用:被子类重写
  • 抽象类的应用:模板方法模式
  • 使用场景:当系统中出现同一个功能多处在开发,而该功能中大部分代码是一样的,只有其中部分可能不同的时候
  • 步骤:
    • 1、把功能定义成一个所谓的模板方法,放在抽象类中,模板方法中只定义通用且能确定的代码。
    • 2、模板方法中不能决定的功能定义成抽象方法让具体子类去实现。
  • 注意:模板方法我们是建议使用final修饰的,因为模板方法是给子类直接使用的,不是让子类重写的,
    一旦子类重写了模板方法就失效了
  • 案例:不同银行账户有不同的登录后计算利息方法,
    • 解决:
      1、可以将账户定义成为统一的模板类型即抽象类
      2、将登陆定义为模板方法(final修饰,子类可直接调用),计算利息方法为抽象方法(子类重写)。登陆方法中会调用计算利息方法。
      3、定义具体类型账户继承抽象类,重写计算利息方法

2. 接口

  • 概念:是一种规范,用抽象方法定义的一组行为规范

  • 声明

    //接口用关键字interface来定义
    public interface 接口名 {
       // 常量
       // 抽象方法
    } 
    
  • 成员:
    抽象方法:public abstract修饰的,可省略
    常量:

  • 用法:用来被类实现(implements)的,实现接口的类称为实现类。实现类可以理解成所谓的子类

    //实现的关键字:implements
    修饰符 class 实现类 implements 接口1, 接口2, 接口3 , ... {
    }
    
  • 特点:

    • 1、不可被实例化
    • 2、接口可被多实现。
      多个接口中有同样的静态方法:只能接口自己调用,实现类不可调用
      同名的默认方法不冲突,需要重写
    • 3、接口可多继承
      如果多个接口中存在规范冲突则不能多继承。如返回值不同方法名相同
    • 4、类即可实现接口,又可继承父类
      接口和父类中有同样的静态方法,父类方法优先
  • jdk8新特性

    • 情况:当接口需要新增10个抽象方法时,此时所有实现该接口的类都需要重写该方法。jdk8后可以允许接口直接定义带有方法体的方法
    • 解决
      • 默认方法:用default修饰的普通实例方法,默认会public修饰
      • 静态方法:用static修饰的静态方法,默认会public修饰
      • 私有方法(jdk9):用private修饰的私有的实例方法,只能在本类中被其他的默认方法或者私有方法访问

5.4 关键字

final和finally、finalize

  • final

    • 含义:最终
    • 修饰范围:
      • 1、修饰方法:表明该方法是最终方法,不能被重写。
      • 2、修饰变量:表示该变量第一次赋值后,不能再次被赋值(有且仅能被赋值一次)。
        变量是基本类型:那么变量存储的数据值不能发生改变。
        变量是引用类型:那么变量存储的地址值不能发生改变,但是地址指向的对象内容是可以发生变化的
      • 3、修饰类:表明该类是最终类,不能被继承。
        abstract:抽象类的抽象方法,不可修饰变量、代码块、构造器
        final 和 abstract:互斥关系,final不可被继承,abstract用来被继承
  • finally:try catch后,无论是否有异常都要执行

  • finalize:Object类的方法,整理系统资源或执行清理工作

throw和throws

throw和throws的区别:
throw:在方法内部直接创建一个异常对象并抛出
throws:在方法声明上,抛出内部的异常

volatile和transient

  • volatile修饰的变量:程序快速运行时,读取该变量不会从缓存中读取,而是直接读取该变量最新值,但是降低了性能。
    • 程序访问变量机制:当程序有执行其他慢操作(如打印或休眠)时,就不会直接读取缓存,这时候可以不加关键字
    • volatile vs synchronized:
      volatile:仅仅保证可见性,无法保证互斥性,所以不安全
      synchronized:不仅有可见性,还有保正了互斥性
  • transient

static、instanceof

  • 用法:
    • 静态变量、静态方法:为类共享的资源
    • 静态代码块:用于初始化操作
      public class PreCache{
      	static{
      		//操作
      	}
      }
      
    • 静态内部类
    • 静态导包:import.static 包名:导入该包中所有的静态资源
  • instanceof关键字
    • 作用:测试一个对象是否为一个类的实例
    • 格式:boolean result = obj instanceof ClassName
      obj: 必须为引用类型,null也不行
      boolean result = s1 instanceof Student;

六、异常Throwable

throw和throws的区别: throw在方法内部直接创建一个异常对象并抛出;throws在方法声明上,抛出内部的异常

6.1 异常体系

不处理异常:程序退出jvm暂停运行

  • 错误Error:系统级别错误,编译时不检查。如jvm退出,代码无法控制,内存溢出
  • 异常类Exception
    • 运行时异常RuntimeException
      • 特点:编译阶段不报错,即写代码时不会报红。
        程序员业务没有考虑好,逻辑不严谨的错误
      • 例如:空指针异常,数组下标越界,数字操作异常。类型转换异常,数字转换异常
    • 编译时异常
      • 特点:无法通过编译阶段,即写代码时会报红
        程序员抛出异常才能解决报红。throws
      • 例如:简单日期格式化类、IO异常、文件不存在异常,SQL语句异常

6.2 异常处理机制

  • 默认处理机制
    • 缺点:一旦真的出现异常,程序立即死亡!
    • 步骤:
      • 默认会在出现异常的代码那里自动的创建一个异常对象:ArithmeticException。
      • 异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给JVM虚拟机。
      • 虚拟机接收到异常对象后,先在控制台直接输出异常栈信息数据。
      • 直接从当前执行的异常点干掉当前程序。
      • 后续代码没有机会执行了,因为程序已经死亡。
        在这里插入图片描述
  • 编译时异常处理机制:
    • 形式一:throws

      • 使用:用在方法上,可以将方法内部出现的异常抛出去给本方法的调用者处理
      • 特点:发生异常的方法自己不处理异常,如果异常最终抛出去给虚拟机将引起程序死亡。后续程序无法执行
      • 格式:
    • 形式二:try…catch…

      • 使用:监视捕获异常,用在方法内部,可以将方法内部出现的异常直接捕获处理
      • 特点:发生异常的方法自己独立完成异常的处理,程序可以继续往下执行
      • 格式:
        在这里插入图片描述
    • 形式三:两者结合(推荐)

      • 使用:
        方法直接将异通过throws抛出去给调用者
        调用者收到异常后直接捕获处理。
  • 运行时异常的处理机制
    • 运行时异常:运行时异常编译阶段不会出错,是运行时才可能出错的,所以编译阶段不处理也可以。
    • 解决:建议在最外层调用处集中捕获处理即可。

6.3 自定义异常

  • 必要性:
    Java无法为这个世界上全部的问题提供异常类。
    如果企业想通过异常的方式来管理自己的某个业务问题,就需要自定义异常类了。
  • 好处:
    a、可以使用异常的机制管理业务问题,如提醒程序员注意。
    b、同时一旦出现bug,可以用异常的形式清晰的指出出错的地方。
  • 步骤:
    • 自定义编译时异常
      • 1、定义一个异常类继承Exception.
      • 2、重写构造器。(无参构造和含message构造)
        public class MyIllegalException extends Exception{
        	public MyIllegalException() {
            }
        	public MyIllegalException(String message) {
        		super(message);
         	}
        }
        
      • 3、在出现异常的地方用throw new 自定义对象抛出
        public class MyTest {
        public static void main(String[] args) throws MyIllegalException {
        
        	//两者结合
        	try{
        		checkAge(1111);
        	}catch(Exception e){
        		e.printStackTrace();
        	}
        	System.out.println("程序结束");
        
        }
        
        public static void checkAge(int age) throws MyIllegalException {
        		if (age < 0 || age > 200){
        			throw new MyIllegalException(age + "不合法。。。。。");
        		}else {
        			System.out.println(age + "合法");
        		}
        	}
        }
        
      • 作用:编译时异常是编译阶段就报错,提醒更加强烈,一定需要处理!!
    • 自定义运行时异常
      • a、定义一个异常类继承RuntimeException.
      • b、重写构造器。
      • c、在出现异常的地方用throw new 自定义对象抛出!
      • 作用:提醒不强烈,编译阶段不报错!!运行时才可能出现!!
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值