Java笔记

String字符串

String是一个类,属于数据类型中的引用类型。

Java的一切使用""引起来的内容,都是这个类的实例,称为字符串对象。

字符串在定义后,值不可改变一个常量,实际是一个字符数组

在这里插入图片描述

String类使用时注意

由此可见,如果要频繁改动String类型变量的值,会创建很多字符串对象,效率很低。

所以在频繁改动字符串时,不要使用String类。

如果要频繁改动字符串时,使用StringBuilder或StringBuffer类。

如何创建字符串对象

1.使用""赋值创建

String str="abc";

2.通过构造方法创建

常用构造方法说明
String()创建一个空白字符串对象,即""
String(String str)创建一个指定字符串对象
String(char[] list)创建一个指定字符数组的字符串对象
String(byte[] bytes,String charsetName)按指定的编码格式创建一个指定字节数组的字符串对象

不同方式创建字符串的过程

使用""赋值的形式创建

//这句话执行时,判断"ab"是否存在于字符串缓冲区中,不存在,创建,将其地址保存到str1中
String str1 = "ab";
//这句话执行时,判断"ab"是否存在于字符串缓冲区中,已存在,将其地址保存到str2中
String str2 = "ab";
//这句话执行时,+两端如果都是""定义的字符串,拼接后再判断"ab"是否存在于字符串缓冲区中,已存在,将其地址保存到str3中
String str3 = "a" + "b";
//以上三句话,只有一个字符串对象创建,即"ab",str1,str2,str3指向了同一个地址,所以用==比较都是true
System.out.println(str1 == str2);
System.out.println(str1 == str3);

在这里插入图片描述

可以使用JDK中自带的反编译工具javap对class文件进行反编译。

在class文件所在目录下(项目的out目录中),进入控制台,输入 javap -c 字节码文件名.class

在这里插入图片描述

使用构造方法String(String str)创建

//这句话的执行流程
//1.在字符串缓冲区中寻找"ab",不存在,创建
//2.在堆中new String()创建对象,将字符串缓冲区中的"ab"的字符串地址保存在new String()的区域中
//3.将堆中new String()整个对象保存到栈中str1变量中
String str1 = new String("ab");
//这句话的执行流程
//1.在字符串缓冲区中寻找"ab",存在
//2.在堆中new String()创建对象,将字符串缓冲区中的"ab"的字符串地址保存在new String()的区域中
//3.将堆中new String()整个对象保存到栈中str1变量中
String str2 = new String("ab");


//以上两句话,在字符串缓冲区中有一个"ab"的字符串,在堆中有两个对象
//str1和str2保存堆中不同的两个地址,所以为false
System.out.println(str1 == str2);

在这里插入图片描述

使用+拼接""和new出来的字符串对象创建

//在字符串缓冲区中创建"ab"
String str1 = "ab";
//1.创建StringBuilder对象
//2.在字符串缓冲区中创建"a"
//3.在字符串缓冲区中创建"b"
//4.在堆中new String(),将"b"保存在其中
//5.调用StringBuilder对象的append()方法,将"a"和new String("b")拼接
String str2 = "a" + new String("b");//一共会创建"a","b",new String(),new StringBuilder()四个对象

//两个不同的地址
System.out.println(str1==str2);

在这里插入图片描述

总结

在使用字符串时,字符串是对象,如果要比较其值是否相同,不能使用判断,因为判断的是内存地址。

所以在比较字符串是否相同时,要使用String类重写的equals方法进行判断。

String类中equals重写的原理大致是:

判断是否为同一个字符串,再判断是否是字符串类型,再将两个字符串转换为字节数组,逐一比较字节数组中的内容,全部一致,返回true

调用equals方法时,通常将已知非空字符串作为调用者

username.equals("admin");//username可能为空,会有空指针异常

"admin".equals(username)//能避免空指针异常

字符串相关面试题

//题目一
String str1="ab";//字符串缓冲区中创建"ab"
String str2="ab";//使用字符串缓冲区中的"ab"
String str3="a"+"b";//使用字符串缓冲区中的"ab"
String str4=new String("ab");//使用字符串缓冲区中的"ab",将其保存在new String()中
String str5="a"+new String("b");//创建"a"和"b",将"b"保存在new String(),将"a"和new String()保存在StringBuidler对象中

System.out.println(str1==str2);//true
System.out.println(str1==str3);//true
System.out.println(str1==str4);//false
System.out.println(str1==str5);//false


//题目二
String s1="abc";
String s2="a"+"b"+"c";

//以上两句话执行后会创建几个对象?一个"abc"

//题目三
String str=new String("hello");
//这句话执行时会创建几个对象?
//1个或2个
//如果字符串缓冲区中有"hello",只会创建new String(),这时只创建一个对象
//如果字符串缓冲区中没有"hello",会创建"hello"和new String(),这时会创建两个对象


//题目四
String s3=new String("wor");//堆中new String() 常量池"wor"
String s4=s3+"ld";//堆中new StringBuilder()  常量池"ld"
//以上两句话会创建几个对象?
//4个
//+两端都是""赋值的字符串,拼接发生在编译阶段,将最终拼接的结果保存在字符串缓冲区中
//其余情况都会创建StringBuilder拼接字符串

字符串String类中的常用方法

方法名返回值作用
length()int得到字符串长度
toLowerCase()String转换为小写
toUpperCase()String转换为大写
trim()String去除字符串的首尾全部空格
isEmpty()boolean判断字符串长度是否为0
getBytes()byte[]转换为字节数组
toCharArray()char[]转换为字符数组
equalsIgnoreCase(String str)boolean忽略大小写比较字符串是否相同
equals(String str)boolean判断两个字符串是否相同
charAt(int index)char得到某个索引上的字符
indexOf(String str)int得到某个字符串第一次出现的索引,不存在返回-1
lastIndexOf(String str)int得到某个字符串最后一次出现的索引,不存在返回-1
contains(String str)boolean判断是否存在某个字符串
startsWith(String str)boolean判断是否以指定字符串开头
endsWith(String str)boolean判断是否以指定字符串结尾
concat(String str)String将指定字符串拼接到原字符串末尾
substring(int index)String从索引index开始截取字符串至末尾
substring(int begin,int end)String截取[begin,end)范围内的字符串
split(String regex)String[]根据字符串或正则表达式切分原字符串。相当于split(regex,0)
split(String regex,int limit)String[]根据字符串或正则表达式切分原字符串,limit为正数,数组长度最大为limit;limit为0,数组长度无限制,但省略最后的空字符串;limit为负数,数组长度无限制。
replace(String oldStr,String newStr)String将原字符串中的oldStr替换为newStr
replaceFirst(String oldStr,String newStr)String将原字符串中第一次出现的oldStr替换为newStr
String.valueOf(参数)String将参数转换为字符串。参数可以是任何数据。通常用于原始类型转换为字符串
String.format(String 格式,Object…obj)String根据指定格式转换参数。常用与将浮点数据保留指定小数位数。\n如String.format(“%4.2f”,2.345)表示将2.345保留2位小数,整体占4位,输出为字符串格式。如果实际数字总位数大于4,原样输出,如果实际数字总位数小于4,会在最前补充空格。

可变字符串

String字符串对象是一个常量,在定义后,值不可改变。

如果使用String类的对象,对其频繁更新时,就会不停地创建新对象,不停引用给同一个变量。

如果要执行10000次循环重新赋值的过程,就会创建10000个字符串对象,效率很低,这是就需要使用可变字符串对象。

public static void main(String[] args) {

    System.out.println("程序开始执行");

    String str = "hello";
    //从1970/1/1 0:8:0   至今经过了多少毫秒
    //记录开始时间
    long startTime = System.currentTimeMillis();
    //创建一个可变字符串对象
    StringBuilder sb = new StringBuilder("hello");
    //频繁更新字符串
    for (int i = 0; i < 5000000; i++) {
        //循环多少次,就会创建多少个String对象,每个对象的创建需要时间和空间
        //str += i;//实际不是更新字符串,而是创建字符串
        //全程只有一个对象参与,每次循环只是在操作该对象
        sb.append(i);
    }
    //记录结束时间
    long endTime = System.currentTimeMillis();
    System.out.println("程序执行完毕");
    System.out.println("用时"+(endTime-startTime)+"毫秒");
}

StringBuilder

用于表示可变字符串的一个类,是非线程安全的,在单线程环境下使用,效率更高。

StringBuffer

用于表示可变字符串的一个类,是线程安全的,在多线程环境下使用。

StringBuilder和StringBuffer中的方法都一致,只不过StringBuffer中的方法使用了synchronized关键字修饰,表示是一个同步方法,在多线程环境下不会出现问题。

这里以StringBuilder为例

构造方法

常用构造方法作用
StringBuilder()创建一个大小为16的字节数组,表示一个空白字符串。
StringBuilder(int capacity)创建一个指定大小的字节数组,表示一个空白字符串。
StringBuilder(String str)创建一个str的长度+16大小的字节数组。表示str这个字符串。

常用方法

常用方法作用
append(Object obj)将任意数据添加到原可变字符串末尾
delete(int start,int end)删除[start,end)范围内的字符
deleteCharAt(int index)删除index索引上的字符
insert(int index,Object obj)将obj添加到index上
replace(int start,int end,String str)将[start,end)范围内的字符替换为str
reverse()反转字符串

注意

  • String类中的所有方法调用后,都会创建一个新的String对象,即原本的String字符串不会改变

  • StringBuilder类中的所有方法都是在操作同一个字符串对象,每次调用方法,都会让原字符串发生变化

  • StringBuilder类中没有重写equals方法,所以判断两个可变字符串对象是否相同时,如果调用equals方法,

    实际调用的是Object类中未重写的方法,即==判断。所以判断可变字符串是否相同时,需要将其转换为String对象再调用equals方法。

可变字符串与String之间的转换

String转换为可变字符串

String str="hello";
//使用构造方法将String对象转换为StringBuilder对象
StringBuilder sb = new StringBuider(str);

可变字符串转换为String(任意类型对象转换为String)

  • String.valueOf(Object obj)方法

    StringBuilder sb = new StringBuider("hello");
    //将任意类型对象转换为String对象
    String str = String.valueOf(sb);
    
  • toString()方法

    StringBuilder sb = new StringBuider("hello");
    //调用任意对象的toString()方法
    String str = sb.toString();
    
  • 拼接空字符串

    StringBuilder sb = new StringBuider("hello");
    String str = sb+"";
    

可变字符串相关面试题

比较String、StringBuilder和StringBuffer的区别

相同点:

  • 这三个类都可以表示字符串。都提供了一些操作字符串的方法。
  • 这三个类中有相同的方法,如charAt(),indexOf()等。
  • 这三个类都是被final修饰的类,不能被继承

不同点:

  • String定义的字符串是一个常量。可变字符串定义的字符串是一个变量。
  • String类中的方法调用后,不会改变原本字符串的值。可变字符串中的方法调用后,会改变原本字符串的值
  • StringBuilder是非线程安全的可变字符串类,StringBuffer是线程安全的可变字符串类,其中的方法被synchronized修饰。

总结

  • 在频繁操作同一个字符串时,一定使用StringBuilder或StringBuffer对象。
  • 操作不经常改动的字符串,使用这三个类中相应的方法处理。

作业

  • 将一个手机号中的4位替换为*,如13893677552,转换为138****7552

  • 根据身份证号,输出这个人的生日和性别。倒数第二位为奇数表示男

  • 将一个字符串反转输出how are you,输出uoy era woh

  • 给定一段字符,得到某个字符出现的次数。如how are you,输入o,输出出现2次

  • 根据顶级域名判断属于什么网站 如 .com表示商业网站 .edu教育 .gov政务

System类

这个类中包含了一些系统相关的信息和一些方法。其中的属性和方法都是静态的。

这类不能创建对象,不是因为它是以抽象类,而是因为它的构造方法是私有的。

常用方法和属性

常用方法和属性
System.ou获取标准输出流对象,用于打印信息
System.in获取标准输入流对象,用于获取输入的信息
System.err获取错误输出流对象,用于打印异常信息
System.exit(int statues)终止虚拟机运行,参数0表示正常终止
System.currentTimeMills()获取从1970/1/1 0:0:0至今经过了多少毫秒。中国是UTC(+8),所以实际是从1970/1/1 8:0:0至今经过了多少毫秒。返回值为long类型。通常称为时间戳。
System.arraycopy(原数组,原数组的起始位置,目标数组,目标数组的起始位置,要复制的元素数量)将原数组中指定数量的元素复制到新数组中

Runtime类

Runtime类的对象,表示程序运行时对象(程序运行环境对象)。

包含了程序运行环境相关的信息。常用语获取运行环境信息(如虚拟机信息),或执行某个命令。

特点

这个类不是一个抽象类,但不能创建对象,因为它的构造方法是私有的。

这个类提供了一个静态方法getRuntime(),通过该方法,可以获取一个Runtime类的对象。

这种方式可以保证该类只能创建一个对象,是Java中的一种设计模式:单例模式

public class Runtime{
    //定义了一个私有的静态成员,创建一个当前类的对象
    private static Runtime currentRuntime = new Runtime();
    //将构造方法私有,无法在外创建对象
    private Runtime();
    //定义了一个公开的静态方法,用于获取创建的唯一的当前类的对象
    public static Runtime getRuntime(){
        return currentRuntime;
    }
}

使用

public class Test {
    public static void main(String[] args) throws IOException, InterruptedException {

        //如果要使用Runtime类中的方法,先通过其静态方法获取它的对象
        Runtime runtime = Runtime.getRuntime();

        System.out.println("当前虚拟机的空闲内存"+runtime.freeMemory() / 1024 / 1024 + "MB");//b  kb  MB
        System.out.println("当前虚拟机的实际最大内存"+runtime.totalMemory()/ 1024 / 1024 + "MB");
        System.out.println("当前虚拟机的支持的最大内存"+runtime.maxMemory()/ 1024 / 1024 + "MB");

        //exec(String 指令名) 运行某个指令,返回运行的进程对象
        //mspaint画图  calc计算器 notepad记事本
        Process mspaint = runtime.exec("mspaint");
        Thread.sleep(3000);
        //销毁进程对象
        mspaint.destroy();
        /*runtime.exec("calc");
        runtime.exec("notepad");*/
        //定时关机
        //runtime.exec("shutdown -s -t 600");
        //runtime.exec("C:\\Program Files (x86)\\Tencent\\QQ\\Bin\\QQ.exe");

    }
}

Date类

date日期 data数据

用于表示日期时间的类,位于java.util包下

构造方法

常用构造方法说明
Date()创建当前时间对应的日期对象
Date(long l)创建指定瞬间对应的日期对象
Date(int year,int month,int date)根据年月日创建日期对象。

常用方法

常用方法作用
getTime()得到Date对应对象的毫秒数
after(Date when)判断参数是否在调用日期之后
before(Date when)判断参数是否在调用日期之前

SimpleDateFormat类

用于格式化日期的类。

构造方法

常用构造方法作用
SimpleDateFormat(String pattern)创建一个指定日期模板的格式化日期对象

日期模板

特殊字符作用
yyyy
MM
dd
hh12小时制
HH24小时制
mm
ss
E星期
yyyy/MM/dd HH:mm:ss E2023/03/09 14:05:16 周四

两个字母都可以写成一个,如5月,M–5,MM–05

常用方法

常用方法返回值作用
format(Date date)String将Date对象按日期模板转换为字符串
parse(String str)Date将满足日期模板的字符串转换为Date对象
package com.hqyj.dateTest;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Scanner;

public class DateFormatTest {
    public static void main(String[] args) throws ParseException {


        //根据指定模板创建格式化日期对象
        //SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM/dd HH:mm:ss E");
        //format(Date date)将Date对象格式化,返回格式化后的字符串
        //String format = sdf.format(new Date());
        //System.out.println(format);

        //SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
        //将满足日期模板的字符串转换为Date对象
        //Date birthday = sdf.parse("1999/9/9");
        //System.out.println(birthday);

        //计算两个输入的日期之间相隔的天数
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入第一个日期,格式如  1999/5/21");
        String dateString1 = sc.next();
        System.out.println("请输入第二个日期,格式如  1999/5/21");
        String dateString2 = sc.next();
        //将日期字符串转换为Date对象
        Date date1 = sdf.parse(dateString1);
        Date date2 = sdf.parse(dateString2);

        //得到日期对应的毫秒数
        long l = date1.getTime() - date2.getTime();
        System.out.println("相隔" + (Math.abs(l) / 1000 / 3600 / 24) + "天");
    }
}

Calendar类

表示日历的类,包含了很的多日历相关的信息。

是一个抽象类,无法创建对象,可以通过静态方法getInstrance()获取Calendar类的实例。

//获取Calendar类的实例
Calendar c = Calendar.getInstance();

日历字段

在Calendar类中,定义了很多被final static修饰的静态常量,称为日历字段。实际是一个数字,用于获取指定日历信息。

常用方法

常用方法作用
get(int field)根据日历字段获取对应的值
getMaximum(int field)获取指定日历字段的最大值,如日期最大为31
getActualMaximum(int field)获取指定日历字段的实际最大值,如11月的日期最大为30
getTime()将Calendar对象转换为Date对象
set(int field,int value)将指定的日历字段设置为指定值
set(int year,int month,int date)同时设置日历的年月日
setTime(Date date)将Date对象作为参数设置日历的信息

使用Calendar类实现万年历

package com.hqyj.dateTest;

import java.util.Calendar;
import java.util.Scanner;

public class Test2 {
    public static void main(String[] args) {
        //实现"万年历"
        //输入年份和月份,输出
        Calendar c = Calendar.getInstance();
        Scanner sc = new Scanner(System.in);
        System.out.println("输入年份");
        int year = sc.nextInt();
        System.out.println("输入月份");
        int month = sc.nextInt();
        //日历设置为当月1号
        c.set(year, month - 1, 1);

        //得到当月最大日期
        int maxDate = c.getActualMaximum(Calendar.DATE);
        //换行计数
        int count = 0;
        //输出空格
        /*
         * DAY_OF_WEEK       星期         空格数量
         * 2                  一          0
         * 3                 二           1
         * 4                 三           2
         * 5                 四           3
         * 6                 五           4
         * 7                 六           5
         * 1                 天           6
         * //周天空6 其余空DAY_OF_WEEK-2
         * */
        System.out.println("一\t二\t三\t四\t五\t六\t日");
        //获取当月1号是一周中的第几天
        int week = c.get(Calendar.DAY_OF_WEEK);
        //周天空6格
        if (week == 1) {
            System.out.print("\t\t\t\t\t\t");
            //空格也需要计数
            count += 6;
        } else {
            //其他情况空星期-2个格
            for (int j = 1; j <= week - 2; j++) {
                System.out.print("\t");
                //空格也需要计数
                count++;
            }
        }
        //输出数字
        for (int i = 1; i <= maxDate; i++) {
            System.out.print(i + "\t");
            //计数+1
            count++;
            //计数到7换行
            if (count % 7 == 0) {
                System.out.println();
            }
        }

    }
}

作业

  • 实现"公告管理"。公告包含编号、标题、内容、作者、发布时间

    • 发布公告:只需输入标题内容和作者,发布时间自动为当前时间
    • 查看公告:输出公告所有信息,时间输出格式为 年月日时分秒
  • 假设从2023年3月1日起,实施"三天上班两天摸鱼"计划,即1、2、3号上班,4、5号摸鱼,

    以此类推,输入之后的一个日期,判断这一天要上班还是摸鱼

    方法调用时传值问题

    package com.hqyj.methodTest;
    
    public class Main {
    
        /*
        * 参数是原始类型,方法内部对参数重新赋值,实参没有影响
        * */
        public static void fun1(int i) {
            i = 123;
            System.out.println(i);
        }
        /*
         * 参数是String,方法内部对参数重新赋值,实参没有影响
         * */
        public static void fun2(String str) {
            str = "new str";
            System.out.println(str);
        }
    
        /*
        * 参数是引用类型,方法内部在操作参数,实参受影响
        * */
        public static  void fun3(Person p){
            p.setName("吴彦祖");
            System.out.println(p.getName());
        }
    
        /*
         * 参数是引用类型,方法内部对形参重新赋值后操作,实参没有影响
         * */
        public static  void fun4(Person p){
            p=new Person();
            p.setName("吴彦祖");
            System.out.println(p.getName());
        }
    
        /*
        * 参数是引用类型,方法内部在操作参数,实参受影响
        * */
        public static  void fun5(int[] nums){
            nums[0]=123;
            System.out.println(nums[0]);
        }
    
    public static void main(String[] args) {
        int i=0;
        fun1(i);//123
        System.out.println(i);//0
    
        System.out.println("-------------");
    
        String oldStr="old str";
        fun2(oldStr);//new str
        System.out.println(oldStr);//old str
    
        System.out.println("-------------");
        Person p = new Person();
        p.setName("小王");
        fun3(p);//吴彦祖
        System.out.println(p.getName());//吴彦祖
    
        System.out.println("-------------");
        Person p2 = new Person();
        p2.setName("小李");
        fun4(p2);//吴彦祖
        System.out.println(p2.getName());//小李
    
        System.out.println("-------------");
        int[] nums={1,2,3,4};
        fun5(nums);//123
        System.out.println(nums[0]);//123
    
    }
    

    }

## 总结

**参数为引用类型(类、数组、接口),且在方法中直接操作该参数时,才会对实际参数造成影响。**

fun3(Person p)参数为Person对象,方法中直接调用参数p的xxx方法,是在操作实际参数。

fun5(int[] list)参数为数组,方法中在直接操作数组某个索引对应的元素,是在操作实际参数。

fun2(String str)和fun4(Person p)都在方法中创建另一个新的对象,是在操作方法中创建的对象,不影响实际参数。

```java
public static void fun(char[] list,Person p){
    list[0]='m';//方法内部直接操作实际参数,会影响实际参数
    p = new Person();//方法内部创建了新对象给实参重新赋值,不会影响实际参数
    p.setName("xxx");
}
public static void main(String[] args){
    Person p = new Person();
    p.setName("qwe");
    char[] list ={'a','b','c'};

    fun(list,p);

    System.out.println(p.getName());//qwe
    System.out.println(list[0]);//m
}

包装类

Java是纯面向对象语言,宗旨是将一切事物视为对象处理。

但原始类型不属于对象,不满足面向对象的思想。但原始类型无需创建对象,保存在栈中,效率更高。

为了既保证效率又让原始类型也有对应的类类型,达到"万物皆对象"的理念,所以就有了包装类的概念。

包装类就是原始类型对应的类类型。

包装类常用于字符串与原始类中之间的转换。

在web应用中,从浏览器页面中获取数据提交到服务器,全部都是String类型,所以一定要使用字符串转换为原始类型的方法。

包装类原始类型
Bytebyte
Shortshort
Integerint
Longlong
Floatfloat
Doubledouble
Characterchar
Booleanboolean

特点

  • 八个原始类型中,除了int和char,其余包装类都是将原始类型的首字母改为大写。

    int对应Integer,char对应Character

  • 包装类都是被final修饰的,不能被继承

  • 除了Character类,其余包装类都有两个过时的构造方法,参数为对应的原始类型或字符串

    Character只有一个参数为char类型的构造方法

    构造方法的目的都是将原始类型的数据转换为包装类的对象

  • 除了Character类,其余包装类都有静态方法"parse原始类型单词(String str)",用于将字符串转换为相应的原始类型

    • 数值型的包装类parseXXX()方法,如果参数不是对应的数字,就会抛出NumberFormat异常,如"123a"或"123.4"都会报错
    • boolean的包装类Boolean的parseBoolean()方法,如果参数不是"true"这个单词的四个字母,转换结果都是false
  • 除了Boolean类,其余包装类都有MAX_VALUE和MIN_VALUE这两个静态属性,用于获取对应类型支持的最大最小值

  • 所有包装类都重写了toString(),用于将包装类对象转换为String对象

    字符串与原始类型之间的转换

    字符串转换为原始类型

    使用原始类型对应的包装类,调用parseXXX(String str)方法

    String num="123";
    
    byte b = Byte.parseByte(num);//123
    short s = Short.parseShort(num);//123
    int i = Integer.parseInt(num);//123
    long l = Long.parseLong(num);//123
    
    float f = Float.parseFloat(num);//123.0
    double d = Double.parseDouble(num);//123.0
    
    boolean flag = Boolean.parseBoolean(num);//false
    

    原始类型转换为字符串

  • 使用String类的String.valueOf(Object obj)

    int num=12;
    String str = String.valueOf(num);
    
  • 拼接空白字符串

    int num=123;
    String str = num+"";
    
  • 将原始类型转换为包装类后,调用toString()

    int num=123;
    Integer integer=new Integer(num);
    String str = interger.toString();
    

    装箱和拆箱

    装箱

    拆箱

    自动装箱和拆箱

  • 自动装箱缓冲区

    //i1和i2保存的数字在byte范围[-127,127]内,这个值会共享,只会有一个"100"对象
    Integer i1 = 100;
    Integer i2 = 100;
    System.out.println(i1 == i2);//i1和i2引用同一个地址,结果为true
    
    //i3和i4保存的数字不在byte范围[-127,127]内,会创建对象
    Integer i3 = 128;//128对象
    Integer i4 = 128;//128对象
    System.out.println(i3 == i4);//i3和i4引用不同的地址,结果为false
    System.out.println(i3.equals(i4));//包装类重写了equals,会拆箱后判断,结果为ture
    
    • 使用自动装箱给包装类对象赋值,值的范围在byte范围[-127,127]内,这个值会保存在缓冲区中,如果多个对象都使用这个值,共享这一个数据,使用同一个地址,==判断结果true;值的范围不在byte范围[-127,127]内,就会创建新的包装类对象,会有不同的地址,==判断结果false
    • 引用类型对象比较相同时,不要使用==,包括包装类的对象。比较相同时,使用包装类重写的equals方法

    异常

    当程序没有按开发人员的意愿正常执行,中途出现错误导致程序中断,这种情况,就称为异常。

    学习异常就是认识异常的种类,如何处理异常和避免异常出现。

    异常的产生

    异常在程序中以对象的形式存在。当代码执行过程中出现异常,虚拟机会自动创建一个异常对象,如果没有对该异常对象进行处理,就会导致程序中断,不再执行后续内容。

    异常的分类

    异常在程序中以对象的形式存在,就有相应的类。

    所有的异常类,组成了"异常家族"。

    Error错误

    如果出现xxxError,如StackOverFlowError,栈溢出,无法通过额外的代码解决,只能修改源代码。

    Exception异常

  • RunTimeExcetpion运行时异常

    如果一个异常类属于RunTimeExcetpion异常类的子类,称这个异常为运行时异常可以通过编译,运行时可能抛出异常对象

    常见运行时异常说明出现的情景
    NullPointerException空指针异常如用空对象null调用属性或方法
    IndexOutOfBoundsException索引越界异常如当使用某个带有索引的对象超出范围
    NumberFormatException数字格式异常如调用包装类的parseXX()方法,如果参数不能转换
    InputMismatchException输入不匹配异常如使用Scanner接收控制台输入时,如果输入的数据不是对应的类型
    ClassCastException对象转型异常如Person p = (Person)Dog dog;
    ArithmeticException算术运算异常如0当分母
  • 编译时异常

    如果一个异常类不属于RunTimeExcetpion异常类的子类,称这个异常为编译时异常无法通过编译,必须要处理异常后才能编译运行。

    常见编译时异常说明出现的情景
    IOException输入输出流异常使用流对象
    FileNotFoundException文件未找到以方法的参数为文件对象时
    SQLException数据库相关异常操作数据库时

    处理异常

    通常所说的处理异常,指的是处理Exception类的子类异常。

    处理异常的目的,就是保证程序正常执行。

    方式一:try-catch-finally语句

    这种方式处理异常,无论会不会抛出异常,都能让程序正常执行

    try{
      //可能出错的代码
    }catch(异常类 异常对象){
      //如果try中的代码抛出异常,异常对象属于catch中的异常类型,就会执行这里的代码
    }catch(异常类 异常对象){
      //如果try中的代码抛出异常,异常对象属于catch中的异常类型,就会执行这里的代码
    }...{
    
    }finally{
      //无论程序是否会抛出异常,都要执行这里的代码
    }
    

    执行流程:先执行try中的内容,当出现异常,与后续每个catch中的异常类型进行匹配,如果匹配到对应的类型或异常父类时,执行后续大括号中的内容,最终一定执行finally中的内容。

    try-catch-finally使用时注意
  • 如果代码会抛出多个异常,可以使用多个catch进行捕获。需要将子类异常放在最前,父类异常放在最后

  • try、catch、finally都不能单独使用,try必须配合catch或finally或一起使用

  • 无论try中的代码是否会抛出异常,finally中的代码一定会执行

  • 执行try中的代码是,如果出现异常,就不再执行try中剩余代码

  • try中定义的内容,无法在try之外的地方使用

  • try中如果有return,不影响finally的执行,finally优先于return执行

    方式二:throws关键字

    这种方法,可以让编译时异常通过编译。

    在定义方法的时候,通过该关键字声明方法可能抛出的异常

    用法:方法的参数部分后,添加 throws 异常类型1,异常类型2…

    public class Test{
      //这时该方法就会有一个声明:该方法可能会抛出InterruptedException异常
      public void fun() throws InterruptedException{
          //sleep()方法在源码中声明了可能会抛出InterruptedException异常,
          //InterruptedException异常不是RuntimeException的子类异常,必须要处理才能通过编译
          //要么使用try-catch处理,要么继续声明有异常
          Thread.sleep(500);
      }
    }
    

    throw和throws

  • throws表示用于声明方法有可能出现的异常。使用时写在方法的小括号之后

    public void fun() throws InterruptedException{
        Thread.sleep(500);
    }
    
  • throw用于手动抛出异常对象。使用时,写在方法体中,“throw 异常对象”。

    常用于满足某种条件时,强制中断程序。

    public void fun(){
        throw 
    }
    

    自定义异常

    如果需要在某种情况下中断程序,可以自定义一个异常类。再通过throw关键字手动抛出自定义异常。

    自定义异常步骤

    1.创建一个类,继承某个异常类

  • 如果继承的是RuntimeException,表示自定义的异常类属于运行时异常,该异常对象可以不用处理

  • 如果继承的是非RuntimeException,表示自定义的异常属于编译时异常,该异常对象必须要处理

    2.可选操作。定义带参构造方法,参数为String类型的异常信息,调用父类中的构造方法

    /*
    
  • 自定义异常

  • 只需继承某个异常类即可

  • 是否定义构造方法根据实际情况决定

  • */
    public class MyException extends NullPointerException {

    /*

    • 带参构造,参数为异常信息

    • /
      public MyException(String msg){
      super(msg);
      }
      /

    • 无参构造

    • */
      public MyException(){
      super();
      }
      }

      
      

    作业

    使用面向对象思想模拟ATM取钱。

    如果账号密码输入错误次数到3次,手动抛出异常强制中断程序。

    如果正确,进入系统,可以存钱取钱查看信息,输入金额过程中允许输入字母,提示输入有误,程序正常执行

    # 数组和集合

    数组的特点

    • 数组中保存的元素都是有序的,可以通过索引快速访问
    • 数组中保存的元素都是同一种类型
    • 数组的长度在定义后,无法改变
    • 数组无法获取其中保存的元素实际数量

    集合的特点

    • 能保存一组数据,元素可以有序或无序(存入的顺序和读取的顺序不一致)
    • 集合中保存的元素的数据类型可以不同
    • 集合的容量可变
    • 可以获取集合中保存的元素实际数量

    集合家族(集合框架)

    图上的所有实现类,都是非线程安全的,在多线程环境下使用以上任意集合,都会出现数据不准确的情况。

    Collection接口

    该接口中有两个核心子接口:List和Set。

    这两个接口都可以保存一组元素,List接口保存元素有序可重复Set接口保存元素无序不重复

    Collection接口有一个父接口Iterable,它不是一个集合,而是用于遍历集合的工具接口,包含forEach()和iterator()方法

    常用方法返回值作用
    add(Object obj)boolean将元素添加到集合中
    size()int获取集合中的元素数量
    isEmpty()boolean判断集合是否为空
    clear()void清空集合
    contains(Object obj)boolean判断集合中是否包含指定元素
    remove(Object obj)boolean移除集合中的指定元素
    toArray()Object[]将集合转换为数组
    stream()Stream获取集合的流对象,用于遍历集合
    iterator()Iterator得到集合的迭代器对象,用于遍历集合

    List接口(有序可重复)

    有序集合,元素可以重复,允许保存Null,可以通过索引获取对应的元素。

    List接口在继承Colletion接口后,又拓展了一些操作元素的方法。

    拓展方法返回值作用
    get(int index)Object得到指定索引的元素
    set(int index,Object obj)Object使用obj替换index上的元素,返回被替换的元素
    add(int index,Object obj)void将obj添加到index上
    remove(int index)Object移除指定索引的元素,返回被移除的元素
    indexOf(Obejct obj)int得到obj第一次出现的索引
    lastIndexOf(Object obj)int得到obj最后一次出现的索引
    subList(int from,int to)List截取[from,to)区间内的元素,返回子集合
    List.of(E… element)List根据参数创建一个不可变集合,该集合不能对其中的元素进行修改

    ArrayList实现类(掌握)

    • 采用数组实现的集合
    • 可以通过索引访问元素,可以改变集合大小,如果要在其中插入或删除元素时,会影响后续元素
    • 该集合查询效率高,中途添加和删除元素效率低
    • 集合中保存的都是引用类型。如集合中保存123,保存的不是int类型的123,而是Integer类型的123
    构造方法
    构造方法说明
    ArrayList()创建一个Object类型的空数组。在调后续添加方法时,才会初始化数组大小为10。
    ArrayList(int initialCapacity)创建一个指定容量的Object类型数组,如果参数为负数,会抛出IllegalArgumentException异常
    ArrayList(Collection c)根据指定集合创建Object类型数组
    常用方法

    主要以List接口和Collection接口中的方法为主。

    LinkedList实现类

    • 采用双向链表实现的集合

    • 集合中保存的每个元素称为节点,除首尾节点外,其他节点既保存了自己的数据,还保存了其前后节点的地址

    • 如果在双向链表的结构中进行插入和删除节点的操作时,不会影响其他节点现在的保存位置。

      添加的节点只需记录前后节点的位置接口。

    • 如果要查询某个节点的地址时,需要从头结点或尾节点开始搜索目标节点的位置

    • 双向链表在中间插入和删除数据效率高,随机读取的效率低

    构造方法
    常用构造方法说明
    LinkedList()创建一个空双向链表
    常用方法

    主要以List接口和Collection接口中的方法为主。由于还实现了Deque接口,所以还有一些Deque接口中的方法。如操作首尾节点的方法。

    常用来自于Deque接口中的方法作用
    addFirst(Object obj)添加obj为头结点
    addLast(Object obj)添加obj为尾结点
    getFirst()得到头结点
    getLast()得到尾节点
    removeFirst()移除头结点
    removeLast()移除尾节点

    ArrayList和LinkedList的区别

    • 这两个类都是List接口的实现类,保存的元素有序可重复,允许保存null
    • ArrayList采用数组实现,随机读取效率高,插入和删除效率低,适用于查询
    • LinkedList采用双向链表实现,插入和删除效率高,随机读取效率低,适用于频繁更新集合

    Set接口(无序不重复)

    无序集合,元素不能重复,允许保存null,没有索引

    Set接口中的方法都是继承于Collection接口。

    哈希表hash table

    哈希表,也称为散列表,是一种数据结构,能更快速地访问数据。

    假设原本的数据为左侧的数组,如要查询10,需要遍历数组,效率不高。

    通过一个特定的函数"原始值%5",得到一组新数组,让新数据重新对应元素,保存到新数组中,这个新数组称为哈希表。

    这时如果要查询10,由于哈希函数是10%5得到0,所以直接查询哈希表中0对应的元素即可。

    整个过程中,这个函数称为哈希函数,得到的新数组称为哈希码,新数组称为哈希表,对应关系称为哈希映射。

    这个哈希函数,有一定的几率让多个原始值得到相同的哈希码,这种情况称为哈希冲突(哈希码一致,实际值不同)。

    为了解决哈希冲突,可以使用"拉链法",将冲突的数据保存在对应哈希码之后的链表中。

    哈希码的特点

    • 如果两个对象的hashcode不同,这两个对象一定不同
    • 如果两个对象的hashcode相同,这两个对象不一定相同
      • hashcode相同,对象不同,这种现象称为哈希冲突
      • 通话”和"重地"这两个字符串的hashcode相同,但是两个不同的对象

    HashSet实现类

    • 采用哈希表实现
    • 元素不能重复,无序保存,允许保存一个null
    • 本质是HashMap对象
    • 使用HashSet集合时,通常要重写实体类中的equals和hashcode方法
    构造方法
    常用构造方法说明
    HashSet()创建一个空集合,实际是创建了一个HashMap对象
    常用方法

    HashSet中没有定义属于自定的方法,都是父接口Set和Collection中的方法。

    HashSet添加数据的原理

    如果添加的两个元素的equals方法结果为true且hashcode相同时,视为同一个对象,不能添加。

    每次向集合中添加元素时,先判断该元素的hashcode是否存在

    • 如果不存在,视为不同对象,直接添加
    • 如果存在,再判断equals方法的结果
      • 如果true,视为同一个对象,不能添加
      • 如果false,视为不同对象,可以添加

    由此可见,不能添加的条件是两个对象的hashcode相同且equals的结果为true。

    可以只判断equals的结果。但是如果每次都判断equals,由于重写equals时会判断很多属性,效率不高。

    如果只判断hashcode是否相同,效率高,但可能会出现哈希冲突。

    所以先判断hashcode,再判断equals,既能保证效率,又能保证不会添加重复元素。

    equals和hashcode的关系
    • 如果两个对象的equals方法结果为true,在没有重写equals方法的前提下,hashcode相同吗?

      • 如果没有重写equals方法,默认使用Object中的方法,使用==判断,如果结果为true,说明是同一个地址,同一个对象,hashcode一定相同
    • 如果两个对象的hashcode不同,在没有重写equals方法的前提下,equals方法的结果为?

      • hashcode不同,说明不是同一个对象,没有重写equals,说明使用Object中的方法,使用==判断,结果为false
    • 如果两个对象的hashcode相同,equals方法的比较结果为?

      • 可能为true或false

        String str1="hello";
        String str2="hello";
        //str1和str2使用同一个地址,hashcode相同,equals结果为true
        String str3="通话";
        String str4="重地";
        //str3和str4不是同一个地址,但hashcode相同,这种情况称为哈希冲突,equals结果为false
        
    HashSet的应用

    如果要保存的对象保证不重复,且无关顺序,可以使用HashSet。重写要保存的元素的equals和hashcode方法。

    Student类,保证添加对象时,不重复

    public class Student {
        private int id;
        private String name;
        private String major;
    
        @Override
        public String toString() {
            return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", major='" + major + '\'' +
                '}';
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Student student = (Student) o;
            return id == student.id && Objects.equals(name, student.name) && Objects.equals(major, student.major);
        }
    
        /*
        * 重写hashcode方法,根据所有属性生成哈希码
        * */
        @Override
        public int hashCode() {
            return Objects.hash(id, name, major);
        }
    
        public Student(int id, String name, String major) {
            this.id = id;
            this.name = name;
            this.major = major;
        }
    
        public int getId() {
            return id;
        }
    
        public void setId(int id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {## 标题
            this.name = name;
        }
    
        public String getMajor() {
            return major;
        }
    
        public void setMajor(String major) {
            this.major = major;
        }
    }
    

    StudentManager类

    public class StudentManager {
        private HashSet<Student> hs = new HashSet<>();
    
    //添加时,如果对象的属性都一致,视为同一个对象,不能重复添加
    public void addStudent(Student student){
        hs.add(student);
    }
    

    }

### TreeSet实现类

- 特殊的Set实现类,数据可以有序保存,可以重复,不能添加null

- 采用红黑树(自平衡二叉树)实现的集合

  - 二叉树表示某个节点最多有两个子节点
  - 某个节点右侧节点值都大于左侧节点值
  - 红黑树会经过"变色"和旋转达到二叉树的平衡

- 只能添加**同一种类型**的对象且该对象实现了**Comparable接口**

  - 每次调用add方法添加元素时,会自动调用Comparable接口中的方法compareTo()方法
  - 实现Comparable接口后必须要重写compareTo()方法,用于决定新添加的元素放在旧元素之前或之后

- compareTo方法的返回值决定了能否添加新元素和新元素的位置

  - 如果返回0,视为每次添加都是同一个元素,不能重复添加
  - 如果返回正数,将新元素添加到现有元素之后
  - 如果返回负数数,将新元素添加到现有元素之前

- 添加的元素可以自动排序

#### 构造方法

| 常用构造方法    | 说明      |
| --------- | ------- |
| TreeSet() | 创建一个空集合 |

#### 常用方法

能使用Set和Collection接口中的方法,还定义了一些属于它的方法

| 独有方法                | 作用                 |
| ------------------- | ------------------ |
| first()             | 得到集合中的第一个元素        |
| last()              | 得到集合中的最后一个元素       |
| ceiling(Object obj) | 得到集合中比参数大的元素中的最小元素 |
| floor(Object obj)   | 得到集合中比参数小的元素中的最大元素 |

#### TreeSet的应用

如果要保存的元素需要对其根据某个属性排序,使用该集合。

如在集合中保存整数,即Integer对象,Integer类已经实现了Comparable接口,

如要保存自定义的元素,必须要实现Comparable接口,重写compareTo方法,自定义排序规则。

Person类

```java
public class Person implements Comparable {
    private String name;
    private int age;

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    /*
        根据age属性升序保存
    */
    @Override
    public int compareTo(Object o) {
        return this.getAge() - ((Person) o).getAge();
    }
}

Main类

public class Test {
    public static void main(String[] args) {
        //使用TreeSet保存Person对象,Person有age属性,按age升序保存到集合
        TreeSet<Person> personSet = new TreeSet<>();
        Person p1 = new Person("小王",20);
        Person p2 = new Person("小李",15);
        Person p3 = new Person("小赵",21);
        personSet.add(p1);
        personSet.add(p2);
        personSet.add(p3);

        System.out.println(personSet);
    }
}

Map接口

Map称为映射,该集合中保存的数据是以键值对的形式保存,保存的键与值的映射关系。

键称为Key,值称为Value,键不能重复,允许出现一个null作为键,值没有限制。

键和值都必须是引用类型。

如,yyds – 永远单身 这就是一个映射关系,"yyds"就是键key,永远单身"就是值value

常用方法作用
size()得到键值对的数量
isEmpty()判断是否为空集合
clear()清空所有键值对
put(Object key,Object value)添加一组键值对
get(Object key)根据键得到对应的值
containsKey(Object key)判断是否存在某个键
containsValue(Object value)判断是否存在某个值
keyset()得到键的集合
values()得到值的集合
entrySet()得到键值对的集合
remove(Object key)删除指定的键值对

HashMap实现类

构造方法

常用构造方法说明
HashMap()创建一个大小为16,加载因子为0.75的空集合

常用方法

使用Map接口中的方法

  • JDK1.8之后,HashMap采用"数组+链表+红黑树"实现

    • 当没有出现哈希冲突时,元素保存在数组中
    • 如果出现哈希冲突,在对应的位置上创建链表,元素保存到链表中
    • 如果链表的长度大于8,将链表转换为红黑树

    遍历集合的方式

    遍历List集合

  • 普通for循环

    for(int i=0;i<集合.size();i++){
        元素 变量 =  集合.get(i);
    }
    
  • 增强for循环

    for(数据类型 变量名 : 集合){
        元素 变量 =  集合.get(i);
    }
    
  • forEach()方法

    使用该方法遍历集合时,不要使用add或remove操作,遍历会抛出异常。

    集合.forEach(obj -> {
         元素 变量 =  集合.get(i);
    });
    
  • 迭代器

    //Collection接口有一个父接口Iterable,其中有一个iterator方法用于获取迭代器对象遍历集合
      //所有Collection的子实现类都能调用该方法
    Iterator it = Collection集合.iterator();
    //hasNext()判断是否还有下一个元素
    while(it.hasNext()){
        //next()方法读取该元素
        元素 变量 = it.next();
    }
    

    遍历Set集合

  • 普通for循环无法遍历Set集合,因为元素没有索引

  • 增强for循环

    for(数据类型 变量名 : 集合){
        元素 变量 =  集合.get(i);
    }
    
  • forEach()方法

    集合.forEach(obj -> {
         元素 变量 =  集合.get(i);
    });
    
  • 迭代器

    //Collection接口有一个父接口Iterable,其中有一个iterator方法用于获取迭代器对象遍历集合
    //所有Collection的子实现类都能调用该方法
    Iterator it = Collection集合.iterator();
    //hasNext()判断是否还有下一个元素
    while(it.hasNext()){
        //next()方法读取该元素
        元素 变量 = it.next();
    }
    

    遍历HashMap集合

    Set keySet = 集合.keySets();
    for(Object key :keySet){
      Object value=集合.get(key);
    }
    

    泛型

    一种规范,常用于限制集合中的元素类型。省去遍历集合时转换Object对象的过程

    //默认可以保存任意类型的元素,即Object类型
    List list = new ArrayList();
    list.add(123);
    list.add("hello");
    
    //遍历时只能使用Object类型获取
    for(Object obj : list){
    
    }
    

    使用泛型

    在定义集合时,在集合类或接口后写上**<引用数据类型>**

    集合类或接口<引用数据类型> 集合遍历名 = new 集合实现类();

    //定义只能保存整数的集合,要使用整数的包装类类型
    List<Integer> list = new ArrayList();
    list.add(123);
    //不能添加非整数
    //list.add("hello");
    

    Collections集合工具类

  • Collection是集合的根接口,定义了操作集合中元素的方法。

  • Collections是集合的工具,定义了操作集合中元素的静态方法。

    Collections中的静态方法说明
    Collections.shuffle(List list)打乱集合中元素的顺序
    Collections.sort(List list)对集合中的元素进行排序。元素必须实现Comparable接口。
    Collections.swap(List list,int a,int b)将集合中索引a和b的元素交换位置
    Collections.reverse(List list)反转集合中的元素
    Collections.max(Collection c)得到集合中元素的最大值。元素必须实现Comparable接口。
    Collections.rotate(List list,int distance)将集合中最后distance个元素放在集合最前
    Collections.fill(List list,Object obj)使用obj填充集合

    Arrays数组工具类

    包含了一些操作数组的静态方法

    常用静态方法说明
    Arrays.sort()对数组中的元素升序排序
    Arrays.asList(T… obj)将可变参数转换为ArrayList集合

    集合和数组之间转化

    数组转换为集合

    //调用Arrays工具类的asList(1,2,6,22,11)或asList(数组)
    ArrayList<Integer> list = Arrays.asList(1,2,6,22,11);
    

    集合转换为数组

    ArrayList list = new ArrayList();
    list.add("sdf");
    list.add(123);
    list.add(null);
    //调用集合的toArray()方法
    Object[] objs = list.toArray();
    

    无论是数组转换集合还是集合转换数组,都可以进行遍历。

    如果集合转换为数组,遍历集合,通过索引赋值。

    如果数组转换为集合,遍历数组,通过add()添加元素

    作业

    使用ArrayList集合实现用户购买图书

    管理员登录后,进行图书增删改查

    用户登录后,查看所有和查看详情

    账号密码 身份 0管理员 1用户

    请选择角色

进程和线程

进程Process

进程就是操作系统中正在执行的程序。

一个程序就是一个执行的进程实体对象。

每个运行中的进程,都有属于它独立的内存空间,各个进程之间互不影响。

线程Thread

线程是一个进程中的执行单元,一个进程中可以有多个线程。

多个线程之间可以访问同一个进程中的资源。

每个线程都有一块独立的栈空间,这些线程所在的栈空间位于同一个进程空间中。

多线程

如果一个进程中,同时执行着多个线程,就称为多线程。

多线程可以提高程序执行效率。如多个窗口卖票,就可以加快卖票的效率。

其实每个执行的Java程序,都是多线程执行。main方法所在的线程称为主线程,还有gc线程(守护线程)在同时运行。

如工厂有很多车间,每个车间有很多流水线。

工厂就是内存,车间就是进程,每个流水线就是进程中的一个线程。

并行和并发

并行

各个进程同时执行,称为并行。

并发

多个线程同时执行,称为并发。

同步和异步

同步

所有任务排队执行,称为同步执行。

异步

在执行任务A的同时,执行任务B,称为异步执行。

Java中的线程Thread类

Java中,线程以对象的形式存在。

Thread类表示线程类。

获取线程对象

  • 获取当前正在运行的线程对象

    Thread thread = Thread.currentThread();
    
  • 创建一个线程对象

    常用构造方法说明
    Thread()创建一个线程对象
    Thread(String name)创建一个指定线程名的线程对象
    Thread(Runnable target)根据Runnable对象创建线程对象
    Thread(Runnable target,String name)根据Runnable对象和指定线程名的线程对象

线程类常用方法

常用方法作用
getId()获取线程ID
getName()获取线程名
getPriority()获取线程优先级,默认为5
getState()获取线程状态
setName(String name)设置线程名
setPriority(int i)设置线程优先级,范围在1~10,数字越大优先级越高,越先执行完
isDaemon()判断是否为守护线程
setDaemon(boolean on)参数为true表示设置线程为守护线程
start()让线程进入就绪状态,等待执行
run()在线程获得执行权时要执行的方法(线程要做的事情)
Thread.sleep(long m)让线程休眠m毫秒后继续执行
Thread.currentThread()获取当前执行的线程对象

实现多线程

方式一:继承Thread类

  • 1.创建一个类,继承Thread类
  • 2.重写Thread类中的run()方法,该方法表示当该线程执行时要做的事情
  • 3.创建自定义的线程对象后,调用start()方法,让线程就绪

自定义Thread类的子类(子线程)

/*
- 1.创建一个类,继承Thread类
- 2.重写Thread类中的run()方法,该方法表示当该线程执行时要做的事情
- 3.创建自定义的线程对象后,调用start()方法,让线程就绪
*/
public class MyThread extends  Thread{
    @Override
    public void run() {
        //该线程循环输出1-100
        for (int i = 1; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName()+i);
        }
    }
}

main类(主线程)

public class Main{
    //如果main方法也要作为一个线程使用时,需要先将其他线程先就绪
    public static void main(String[] args) {
        //创建自定义线程对象,调用start()让线程就绪
        new MyThread().start();

        //主线程同样打印1-100
        for (int i = 1; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName()+i);
        }
    }
}

方式二:实现Runnable接口(建议使用)

由于Java中是单继承,如果某个类已经使用了extends继承了另一个类,就无法再继承Thread类。

这时就可以让自定义线程类实现Runnable接口的方式实现多线程。

  • 1.自定义一个类,实现Runnable接口
  • 2.重写run()方法,将多线程要执行的内容写在该方法中
  • 3.创建Runnable接口的实现类对象
  • 4.使用Thread(Runnable target)构造方法,将Runnable接口的实现类对象包装为Thread对象

自定义Runnable接口的实现类

/*
 * 如果一个类已经是其他类的子类,无法再继承Thread
 * 这时实现Runnable接口,重写Run方法
 * 再将该类对象当做Thread(Runnable target)构造方法的参数,包装为线程类
 * */
public class MyThread2 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() +":"+ i);
        }
    }
}

main类

public class Main{
    public static void main(String[] args) {
        //创建两个子线程对象
        //使用new Thread(Runnable target)构造方法,将自定义线程子类包装为Thread
        Thread t1 = new Thread(new MyThread2());
        Thread t2 = new Thread(new MyThread2());
        t1.start();
        t2.start();
    }
}

方式三:使用匿名内部类

如果不想创建Thread的子类或Runnable的实现类,就可以将匿名内部类当做Runnable接口的实现类来使用。甚至可以配合lambda表达式更进一步简化。

public class Test {
    public static void main(String[] args) {
        // new Thread(Runnable target)参数为接口类型,可以使用匿名内部类实现
        //使用匿名内部类实现多线程
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                }
            }
        },"线程A");
        t1.start();

        //使用Lambda表达式简化匿名内部类
        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        },"线程B").start();
    }
}

线程生命周期

线程的初始化到终止的整个过程,称为线程的生命周期

新生状态

当线程对象创建成功后,进入新生状态

就绪状态

当某个线程对象调用了start()方法后,进入就绪状态。

在这个状态下,线程对象不会做任何事情,只是在等待CPU分配时间片。

运行状态

当某个线程对象得到CPU时间片(CPU执行该线程时分配的时间),进入运行状态,开始执行run()方法。

不会等待run()方法执行完毕,只会在指定的时间内尽可能地执行run()方法。

如果多线程下,某个线程对象在调用完run()方法后,就会再进入就绪状态。

阻塞状态

如果某个线程遇到了sleep()方法或wait()方法时,就会进入阻塞状态。

sleep()方法在指定时间后,会让线程重新就绪。

wait()方法只有在被调用notify()或notifyAll()方法将其唤醒后才能重新就绪。

终止状态

当某个线程的run()方法中的所有内容执行完,就会进入终止状态。

守护线程

如果将一个线程设置为setDeamon(true),表示该线程为守护线程。

守护线程会随着其他非守护线程终止而终止。

package com.hqyj.ThreadTest;

/*
* 测试守护线程
* */
public class DaemonTest {

    public static void main(String[] args) {

        //将含有死循环的线程设置为守护线程
        Thread t1 = new Thread(() -> {
            while (true) {
                System.out.println("守护线程运行中。。。");
            }
        });
        //设置为守护线程
        t1.setDaemon(true);
        t1.start();


        /*
        * 普通线程,有限循环
        * */
        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                System.out.println("普通线程运行中。。。"+i);
            }
        }).start();

        //随着有限循环结束,死循环线程也会结束
        //非守护线程结束,守护线程也会结束

    }
}

多线程访问同一个资源

可能出现的问题

如现有10张票,3个窗口同时卖票。

package com.hqyj.ticketTest;

/*
 * 模拟多窗口卖票
 * */
public class TicketShop implements Runnable {
    //共10张票
    private int ticket = 10;

    /*
     * 卖票的方法
     * */
    public void sell() throws InterruptedException {
        while (true) {
            if (ticket > 0) {
                ticket = ticket - 1;
                System.out.println(Thread.currentThread().getName() + "售出一张,剩余" + ticket + "张");
                Thread.sleep(1000);
            } else {
                System.out.println(Thread.currentThread().getName() +"已售罄");
                break;
            }
        }
    }

    /*
     * 该线程的任务是调用sell()方法
     * */
    @Override
    public void run() {
        try {
            sell();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        //创建一个票房对象,共10张票
        TicketShop ts = new TicketShop();
        //创建3个线程对象模拟3个窗口卖票
        Thread t1 = new Thread(ts,"窗口A");
        Thread t2 = new Thread(ts,"窗口B");
        Thread t3 = new Thread(ts,"窗口C");

        t1.start();
        t2.start();
        t3.start();
    }
}

出现问题的原因

由于线程调用start()方法后,就会进入就绪状态。当该线程获得了CPU时间片,就开始执行run()方法,

并不会等待run()执行完毕,所以很有可能在线程A执行run()方法的途中,线程B也开始执行run()方法,造成数据共享时出错。

如何解决

如果要防止在线程A执行途中线程B插队的情况,可以让所有线程同步(排队)执行。

这样一来,某个线程执行run()方法的时候,其他线程就会等待run()执行完毕。

synchronized关键字

这个关键字可以修饰方法或代码块

修饰方法

写在方法的返回值前,这时该方法就称为同步方法

 public synchronized void sell(){
     //该方法执行途中,其他线程无法插队
 }

修饰代码块

synchronized(要同步的对象){
    //该方法执行途中,其他线程无法插队
}

原理

每个对象默认都有一个"锁"。当某个线程运行到被synchronized修饰的方法时,

该对象就会拥有这把锁,在拥有锁的过程中,其他线程不能同时访问该方法,只有等待其执行结束后,才会释放这把锁。

使用synchronized修饰后的锁称为"悲观锁"。

方法被synchronized修饰后,称为同步方法,会让原本多线程执行变成了单线程执行(异步变同步)。

多线程相关面试题

  • 实现多线程的方式

    • 继承Thread类,创建对象
    • 实现Runnable接口后,包装成Thread对象
    • 匿名内部类
  • 为什么说StringBuilder或ArrayList、HashMap是非线程安全的

    public class Test {
        public static void main(String[] args) throws InterruptedException {
            //使用StringBuilder时,很大概率最终数值不准确,非线程安全
            //StringBuilder sb = new StringBuilder();
            //使用StringBuffer时,没有问题,线程安全
            StringBuffer sb = new StringBuffer();
    
            //创建10个线程对象,创建后进入就绪状态
            for (int i = 0; i < 10; i++) {
                new Thread(() -> {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    //每个线程对象添加字符串
                    for (int j = 0; j < 10; j++) {
                        sb.append("hello");
                    }
                }).start();
            }
            Thread.sleep(5000);
            //如果没有任何问题,每个线程拼接50个字母,10个线程拼接500个,sb的长度为500
            System.out.println(sb.length());
        }
    }
    
  • 什么叫死锁?怎么产生?如何解决?

    如两个人吃西餐,必须有刀和叉,当前只有一副刀叉。

    如果A拿到了刀,B拿到了叉,互相都在等待对方释放另一个工具,如果都不释放,就进入僵持的局面,这个局面就称为死锁,既不结束,也不继续。

模拟死锁出现的情况

定义两个线程类,线程A先获取资源A后,再获取资源B;线程B先获取资源B后,再获取资源A。

PersonA线程

public class PersonA implements Runnable {
    //定义公用的成员变量,刀和叉
    private Object knife;
    private Object fork;

    public PersonA(Object knife, Object fork) {
        this.knife = knife;
        this.fork = fork;
    }

    /*
     * 该线程执行run()方法时,先获取knife资源,在获取fork资源
     * */
    @Override
    public void run() {
        //先获取knife资源
        synchronized (knife) {
            System.out.println(Thread.currentThread().getName() + "已获得刀,2s后得到叉");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //再获取fork资源
            synchronized (fork) {
                System.out.println(Thread.currentThread().getName() + "已获得刀和叉,可以吃饭了");
            }
        }
    }
}

PersonB线程

public class PersonB implements Runnable {
    //定义公用的成员变量,刀和叉
    private Object knife;
    private Object fork;

    public PersonA(Object knife, Object fork) {
        this.knife = knife;
        this.fork = fork;
    }

    /*
     * 该线程执行run()方法时,先获取fork资源,在获取knife资源
     * */
    @Override
    public void run() {
        //先获取fork资源
        synchronized (fork) {
            System.out.println(Thread.currentThread().getName() + "已获得叉,2s后得到刀");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //再获取knife资源
            synchronized (knife) {
                System.out.println(Thread.currentThread().getName() + "已获得刀和叉,可以吃饭了");
            }
        }
    }
}

测试

public class DeathLockTest {
    public static void main(String[] args) {
        Object knife = new Object();
        Object fork = new Object();

        new Thread(new PersonA(knife,fork,paper),"小王").start();
        new Thread(new PersonB(knife,fork,paper),"老李").start();
        //运行后,程序就会卡住,既不继续,也不停止,也不报错。
    }
}

死锁的解决方式

方式一让两个线程获取资源的顺序保持一致

如两个线程都先获取knife,再获取fork

方式二:在两个线程获取的资源A和B之前,再获取第三个资源C,对这个资源C使用synchronized进行同步

这样在某个线程获取资源C后,继续执行后续的内容,知道执行完毕,其他线程才有机会开始执行。

如在获取knife和fork之前,先获取paper对象
private Object fork;

public PersonA(Object knife, Object fork) {
    this.knife = knife;
    this.fork = fork;
}

/*
 * 该线程执行run()方法时,先获取knife资源,在获取fork资源
 * */
@Override
public void run() {
    //先获取knife资源
    synchronized (knife) {
        System.out.println(Thread.currentThread().getName() + "已获得刀,2s后得到叉");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //再获取fork资源
        synchronized (fork) {
            System.out.println(Thread.currentThread().getName() + "已获得刀和叉,可以吃饭了");
        }
    }
}

}


**PersonB线程**

```java
public class PersonB implements Runnable {
    //定义公用的成员变量,刀和叉
    private Object knife;
    private Object fork;

    public PersonA(Object knife, Object fork) {
        this.knife = knife;
        this.fork = fork;
    }

    /*
     * 该线程执行run()方法时,先获取fork资源,在获取knife资源
     * */
    @Override
    public void run() {
        //先获取fork资源
        synchronized (fork) {
            System.out.println(Thread.currentThread().getName() + "已获得叉,2s后得到刀");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //再获取knife资源
            synchronized (knife) {
                System.out.println(Thread.currentThread().getName() + "已获得刀和叉,可以吃饭了");
            }
        }
    }
}

测试

public class DeathLockTest {
    public static void main(String[] args) {
        Object knife = new Object();
        Object fork = new Object();

        new Thread(new PersonA(knife,fork,paper),"小王").start();
        new Thread(new PersonB(knife,fork,paper),"老李").start();
        //运行后,程序就会卡住,既不继续,也不停止,也不报错。
    }
}

死锁的解决方式

方式一让两个线程获取资源的顺序保持一致

如两个线程都先获取knife,再获取fork

方式二:在两个线程获取的资源A和B之前,再获取第三个资源C,对这个资源C使用synchronized进行同步

这样在某个线程获取资源C后,继续执行后续的内容,知道执行完毕,其他线程才有机会开始执行。

如在获取knife和fork之前,先获取paper对象

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值