java开发环境搭建——JDK

  • 下载地址:https://www.oracle.com/cn/java/technologies/downloads/#java8-windows

    安装步骤

    1. 打开安装包开始安装,并且记住安装路径

    2. 配置环境变量

      1. 右键“我的电脑”,选择属性,打开高级系统设置,打开环境变量

      2. 新建变量JAVA_HOME,并且填写值,注意替换成你自己的路径:

        C:\Program Files\Java\jre-1.8

      3. 打开Path(如果没有的话就自己点击新建),新增一个值,填写:

        %JAVA_HOME%\bin

      4. 新建变量CLASSPATH,把下面这个填进去,注意看前面有个“.;”不要漏了:

        .;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar

    3. 打开运行窗口,输入cmd打开命令行终端

    4. 输入Java或者Java -version,测试是否安装成功

IDEA的安装使用

  • 下载地址:https://www.jetbrains.com.cn/
  • 修改字体大小:File > Settings > Editor > Font
  • 修改页面主题:File > Settings > Editor > Color Scheme (或者在Color Scheme下面单独修改单个样式)
  • 查看或修改快捷键:File > Settings > Keymap
  • 运行当前的程序:Run图标或者Shift+F10快捷键
  • 控制台面板显示或隐藏:Alt + 4
  • 撤销上一步操作:Ctrl + Z
  • 代码提示:Actrl + Alt + 空格
  • 删除文件:选中文件,可右键打开操作菜单,找到删除,或Delete按键
  • 根据当前的操作场景做预测:Alt + Enter
  • 如果出现了红色波浪,说明这行代码需要检查是否有异常
  • 按住Shift+上下键,可以多选代码行数
  • 注释选中行:Ctrl + /
  • 多行注释(一般用在文件头部注释或者方法注释):在文件头部输入/**,再按下回车,可以使用多行注释
  • 文件重命名:选中文件后Shift+F6,或者右键Refactor > Rename
  • 代码格式化:Ctrl + Alt + L

断点调试(debug)

  1. 在需要断点的代码行号边上点击,可以打上一个红色的断点,表示代码执行到这个地方需要停下来,等我们操作

  2. 点击断点运行按钮,代码开始运行

  3. 代码会停在断点位置,并且弹出断点调试的观察页面,等待我们操作

    1. 此时“Thread&Variable”面板可以观察当前的变量值,以及一些其他数据的变化过程
    2. 可以执行下一步、停止等功能
    3. 代码页面,每一行的尾部也可以观察数据的变化过程
    4. 点击console,可以回到控制台查看输出结果

第一个程序(输出输入)

如何在Java代码中输出内容:
System.out.print("Hello World\n");//在控制台输出 Hello World,并且通过\n换行
System.out.println("123456");//在控制台输出123456,并直接换行
  • 可以多熟悉idea的用法,比如代码自动补齐、错误排查
//创建一个Scanner对象,并且起名为s
Scanner s = new Scanner(System.in);
//读取用户输入的一整行字符串
s.nextLine()

//java会把字符串后面的内容都当成是字符串处理
System.out.println("计算一下3+4的结果是多少:" + 3 + 4);

变量

变量定义规则: <变量类型> <变量名>

int a;
int a = 1; //变量a被赋值为1
int d,e,f;

变量名命名规范

  • 只能用字母、数字、下划线、美元符号($)
  • 数字不能出现在第一个位置
  • java的关键字不能当成变量名
  • 区分大小写

Java中的关键字

abstract、assert、boolean、break、byte、case、catch、char、class、continue、default、do、double、else、enum、extends、final、finally、float、for、if、implements、import、int、interface、instanceof、long、native、new、package、private、protected、public、return、short、static、strictfp、super、switch、synchronized、this、throw、throws、transient、try、void、volatile、while、true、false、null、goto、const。

变量注意事项

  • 没有初始化的变量不能被使用

常量

按照约定俗成的编程习惯,我们一般会使用大写字母、数字、下划线来定义常量

final int B = 1; //使用final定义了一个名为B的常量,它的值不能被改变

8大基本数据类型

在这里插入图片描述

  • 定义一个类型,计算机就会在内存中开辟对应的内存空间,比如定义一个byte就会占用1个字节,换算成位数就是8位
  • byte常用于一些大型数组的使用场景,比如说实用程序读取图片,一般都会把图片转成byte做处理,可以节省空间提升效率
  • 浮点型不建议用在需要精确计算的应用场景下
  • 字符型可以存储任何一个字符。如果强转成int,可以得到对应的十进制
  • 强制转换数据类型,会发生一些问题,所以要谨慎使用

    某些类型后面为什么要加L、f、d?

    • 在Java中,当你声明一个long类型的变量并给它赋值时,如果在数值后面加上"L"或"l",这是为了明确地告诉编译器这个是一个long类型的值,而不是int类型的值。
    • Java对于变量类型是大小写敏感的。因此,单从字面上看,数值"123"会被视为int类型,而"123L"或者"123l"则会被视为long类型。尽管在Java 5之后,编译器已经足够智能,能够在不添加"L"或"l"的情况下自动推断出数值是long类型还是int类型,但是按照Java的编码规范,为了明确和统一,仍然推荐加上L、f、d。

运算符

算术运算符:+ - * / ++ -- %
赋值运算符:=

在这里插入图片描述

递增与递减运算符

  • i++ ++i 都是对变量做自增或者是自减的操作,但使用上会有一些不同:

    //i++ 在表达式计算完之后再做自增操作
    //++i 是在表达式计算之前就做自增操作
    
    sum += number; //等同于 sum = sum + number;
    //同样还有  -= *= /=  都是一样的用法
    
    

逻辑运算符

boolean b = !(x < 0);  //取反操作、非(一元运算符)

true && true //结果为ture
true && false //结果为false
false && false //结果为fase

true || true //结果为ture
true || false //结果为true
false || false //结果为fase

判断

if语句

//写法1
if (判断条件是否成立:true或者false) {
    //true条件成立就执行这里的代码
} 

//写法2 
if (判断条件是否成立:true或者false) {
    //true条件成立就执行这里的代码
} else {
    //false条件不成立执行这里的代码
}

//写法3:一般我们认为一个封号就是一个语句,这种写法因为只有一个封号,也可以理解为是一个语句,只不过分成了两行来写,意思就是如果条件成立就会执行后面这行代码。 但不太推荐这种写法。
 if (判断条件是否成立:true或者false) 
    System.out.println("欢迎下次光临!");
//嵌套使用:else会与最近的if相匹配
if (a > b) {
	if (a > c) {
		System.out.println("比较大的数是:" + a);
    } else {
        ystem.out.println("比较大的数是:" + c);
    }
} else {
    if (b > c) {
		System.out.println("比较大的数是:" + b);
	} else {
		System.out.println("比较大的数是:" + c);
}

//级联if
if (x = 0) {
	f = 1;
} else if (x = 2) {
	f = x + 2;
} else if (x < 0) {
	f = x + 2;
} else {
    f = 3 * x - 1;
}
  • 建议始终在if语句中使用大括号
  • else:总是和最近的if匹配

switch语句

//语法
switch(控制表达式){
	case 常数1:
	//如果控制表达式与常数1匹配,就执行这里的代码
	break;
	case 常数2:
	//如果控制表达式与常数2匹配,就执行这里的代码
	break;
	default:
	//如果控制表达式找不到相匹配的case,就执行这里的代码
	break;	
}
  • 遇到break会退出当前switch,如果没有switch,则会不断地往下执行
  • default可以省略不写

循环

while循环

//while的用法
while(控制循环的表达式){
	//只要控制表达式为true,就会不断执行这里的代码
}

//死循环
while(true){
}

//do while的用法
do{
	//只要控制表达式为true,就会不断执行这里的代码
}while(控制表达式);

do while和while的区别

  • while需要先判断表达式是否成立,只有成立才会执行循环体里面的代码
  • do while,无论如何都会先做一次循环,做完一次循环之后再去判断条件是否成立

for循环

//number开始的时候等于1,当它<=100的时候,进入到循环体,每次循环执行完毕,执行加1
for (int number = 1; number <= 100; number = number + 1) {
	System.out.println(number);
}

for(初始化操作;进入循环的条件;每一次循环都会执行的动作){
	//循环体
}

//for里面的表达式可以被省略
for (; ;){}

for可以不写大括号,但是不建议
for(; ;) 语句;
  1. 初始化操作:设定一个变量,类似于是一个计数器,用来控制循环是否要继续,或者是退出;

    1. int number = 1;
    2. 一般控制循环的变量,不会在循环体外面使用,所以多数时候都在for里面定义。
  2. 循环的条件:和while类似,只有条件满足的时候才会进入循环体,不满足的时候直接跳出循环;

  3. 步进(每一步都会执行的动作):每一轮循环后,都会执行这里的动作,一般情况都在这里控制循环的变量,多数时候加减。

如何选择

  • 知道固定的循环次数,用for;
  • 不知道循环次数,但是知道循环终止的条件,用while;
  • 必须执行一次循环体,do while,哪怕第一次循环判断是false;

break和continue

if(true){
	//可以终止当前的for、while、do while循环,并且不再执行后续的循环
	break;
}
	
if(true){
	////可以终止本次的for、while、do while循环,并且直接开始下一次的循环
	continue;
}

STOP:
for(初始化操作;循环的条件;每一步都会执行的动作){
	//循环体
	for(初始化操作;循环的条件;每一步都会执行的动作){
		if(true){
		//可以控制最外层的循环
		break STOP;
		}
	}
}

数组

<类型>[] <数组名> = new <类型>[长度];
int[] numbers = new int[10];

//已经知道要存多少数据了
int[] number = new int[]{1, 2, 3, 4, 5};

//存
number[0] = 6; 
//取
int i = number[1];



//二维数组的用法
<类型>[][] <数组名> = new <类型>[][];
int[] numbers = new int[2][5];

//四行五列的二维数组
int[][] number3 = new int[][]{
	{1, 2, 3, 4, 5},
	{6, 7, 8, 9, 10},
	{11, 12, 13, 14, 16},
	{11, 12, 13, 14, 15},
};

//存
number[0][2] = 6; 
//取
int i = number[1][3];

//循环遍历二位数组
for (int i = 0; i < number3.length; i++) {
	for (int j = 0; j < number3[i].length; j++) {
		System.out.println(number3[i][j]);
	}
}
  • 必须定义数组的长度,长度可以是个变量,也可以是个正整数

  • 数组存储的数据必须是同类型

  • 数组的第一个索引(下标)是0,最后一个就是length-1

  • 数组的长度是固定的

  • 数组的赋值操作,其实是让数组变量去持有右边的数组本身,让变量成为这个数组的管理者

    • 不能直接做比较,如果想要对比数组是否相等:boolean b = Arrays.equals(数组1,数组2);
    • 如果需要获取一个相同的数组,不能直接赋值,需要循环赋值
  • 二维数组通过两个索引确定一个位置

字符

Unicode

由于计算机在美国发明,大家更多考虑的是英语如何在计算机中表示。从而得到ASCII码表,表示英语字母总共26个,加上特殊字符,128个字符。后来其他国家也陆续有了自己的编码表,标准不统一,于是就会产生一些问题,于是Unicode诞生了,它可以表达非常多的字符,包括数字、符号、英文、汉字、德文、日文等等,总共可以表示65536个字符。

同时,Java支持Unicode和各种编码规则,所以不同编码规范之间可以通过一些算法进行转换,甚至一些编码都是通用的。比如说,在Unicode码中,ASCII码的字符被分配到0x00-0x7F的范围内,所以我们也可以认为,ASCII码是Unicode码的一个子集。

字符型

char c = 'A';
System.out.println((int) c);//输出65
System.out.println('A' > 'a');//比较的是Unicode编码值的大小

//通过一些特性,做大小写转换
char a = 'a';
char a1 = (char) (a + 'A' - 'a');
System.out.println(a1);

逃逸字符

字符 含义 字符 含义
\b 回退一格 \" 双引号
\t 切换到下个表格位 \’ 单引号
\n 换行符 \\ 反斜杠
\r 回车

字符串

//定义字符串的两种方式
String h= "hello World";//更常用,这种定义方式可以称为“字面量”
String h = new String("hello world");

//读取用户输入的字符串
//读取空格、回车、\t之前的数据
String next = scanner.next();
//一整行
String nextLine = scanner.nextLine();

//对比hello2和hello3的内存地址,换句话说,是在对比,这两个变量指向的是不是同一个对象?
System.out.println(hello2 == hello3);
System.out.println(hello2 == hello4);
//单纯的对比hello2和3的内容是否相等
System.out.println(hello2.equals(hello3));
  • 字符串被创建后可以理解为是一个字符串对象,左边的变量只是当前字符串变量的管理者;
  • 字符串比较内容是否相等需要使用equals(),而不能使用==。

两个字符串是否相等?

对于写代码来说,这个问题没什么好纠结的,因为用==来比较两个字符串的场景几乎不存在,但这算是一个面试高频问题,因此也记录一下,请自行阅读。

在 Java 中,当你使用 == 操作符来比较两个字符串对象时,你实际上是在比较它们的引用地址,而不是它们的值。如果两个字符串对象引用的是内存中的同一个对象、同一个内存地址,那么 == 操作符将返回 true。否则,即使两个字符串的内容完全相同,== 操作符也会返回 false

然而,从 Java 7 开始,Java 引入了一种叫做字符串字面量池(String Literal Pool)或者叫做字符串常量池(String Constant Pool)的机制。当你使用字面量(即直接在代码中写下的字符串值)来创建一个字符串对象时,Java 会检查字符串常量池中是否已经有一个相同内容的字符串对象存在。如果存在,Java 就会返回对这个已存在对象的引用,而不是创建一个新的对象。

例如:

String s1 = "hello"; 
String s2 = "hello";
System.out.println(s1 == s2); // 输出 true

在这个例子中,s1s2 都是通过字面量创建的,所以它们引用的是字符串常量池中的同一个对象,因此 s1 == s2 的结果是 true

但是,如果你使用 new 关键字来显式地创建一个新的字符串对象,那么即使这个对象的内容与字符串常量池中的某个对象相同,它们也不会被认为是相同的对象。

例如:

String s1 = new String("hello"); 
String s2 = "hello";
System.out.println(s1 == s2); // 输出 false

在这个例子中,s1 是通过 new 关键字创建的,所以它引用的是一个全新的对象,而不是字符串常量池中的对象。因此 s1 == s2 的结果是 false

需要注意的是,虽然 == 操作符在比较字符串对象时可能会产生混淆,但使用 equals() 方法来比较字符串的值通常是一个更好的选择。equals() 方法会比较两个字符串的内容是否相同,而不是比较它们的引用是否相同。例如:

java复制代码String s1 = new String("hello");  String s2 = "hello";  System.out.println(s1.equals(s2)); // 输出 true

关于类、对象、变量的称呼

在实际开发中,大家口头上都是叫String或者叫字符串,但在这也适当做个区分,如果你觉得费解,可以不必纠结是什么叫法,后面慢慢的就知道了:

  1. 如果没有被使用,只是单纯的提起Java中的String或者是Scanner,那指的就是字符串或者Scanner的这个类;
  2. 像是"hello world"、new String(“hello world”);、new Scanner(System.in)这样被新建出来的东西,可以称呼为字符串对象、scanner对象;
  3. String h= “hello World”;左边的h,我们可以认为是一个字符串变量,变量名是h。
String hello5 = "hello world";       
//输出字符串的长度
System.out.println(hello5.length());
//indexof查找存在的字符串所在的索引,不存在返回-1
System.out.println(hello5.indexOf("p"));
System.out.println(hello5.indexOf("wor"));

//获取o在hello5中的索引
int o = hello5.indexOf("o");
//在hello5中的第o+1的位置开始查找,获取o的索引
System.out.println(hello5.indexOf("o", o + 1));
//获取hello5中,下标为2(位置是3)的字符
System.out.println(hello5.charAt(2));
//使用llld替换hello5中ld,
String newHello5 = hello5.replace("ld", "llld");
System.out.println(newHello5);
//截取hello5的部分字符串,指定下标从3-6
String substring = hello5.substring(3, 6);
System.out.println(substring);
//让hello5中的小写字母转成大写
String upperCase = hello5.toUpperCase();
System.out.println(upperCase);	
  • 字符串的一些操作不会改变原来的字符串,而是会产生新的字符串

包装类型

为了方便我们对基本数据类型做处理,Java为8种数据类型分别提供了对应的包装类型,通过包装类型可以很方便的对基本类型做一些操作,加快编程效率。

int i = 9;
//包裹类型  自动装箱
Integer integer = i;

Integer integer1 = 100;
//自动拆箱
int i1 = integer1;


//给出最小值
System.out.println(Integer.min(10, 9));
int x = Integer.parseInt("0");
System.out.println(x);

//double
System.out.println(Double.sum(6.5, 2.5));
//float
Float.max(10.0f, 9.0f);
//boolean
System.out.println(Boolean.logicalOr(false, true));
//char
System.out.println(Character.isUpperCase('A'));

// 将Integer对象转换为String对象  
String str = num1.toString();  
System.out.println("String representation of num1: " + str);  
  
// 将String对象转换为Integer对象  
int num3 = Integer.parseInt(str);  
System.out.println("Integer value of str: " + num3);  

byte 	对应:Byte
short 	对应:Short
long	对应:Long

JDK17文档

  • 想要熟悉java的常见类的用法:

    • 多写、多模仿
    • 多看文档
  • 推荐文档地址(有其他看的习惯的文档也可以,看得懂就行):https://doc.qzxdp.cn/jdk/17/zh/api/java.base/java/lang/Integer.html

  • 前期不需要一个个的看过去,只关注我们学过的类就可以了,看多了也容易忘,而且还会增加学习难度!

方法

方法也可以叫函数,可以帮我们实现一些特定的需求。如果系统方法无法满足,我们也可以自定义方法

    /**
     * 计算start-end之间所有整数的和
     *
     * @param start 开始
     * @param end   结束
     */
    public static 返回值类型 方法名(int start, int end) {
		//这里面是方法体

        //防止前者比后者大
        if (start > end) {
            int temp = start;
            start = end;
            end = temp;
        }

        int b = 0;
        for (int j = start; j <= end; j++) {
            b += j;
        }
        System.out.println(start + "-" + end + "之间所有整数的和等于:" + b);

	//return 0; 如果有指定返回值类型,就需要return。 如果是void,就不需要
    }

参数列表

调用方法的时候,需要严格按方法的参数列表来传值,匹配对应的数量和类型。

  • 字面量、变量、其他方法的返回值、计算后的结果,都可以直接传递给方法:
sum(1, 2);
sum(max, i);
sum(addExact5(9, 5, 6, 7, 8), 9);
sum(1 + 1, 2 + 2);
  • 调用时传递的参数类型比参数列表定义的类型更小(窄)的时候,编译器会自动匹配、转换;
  • 如果更大(宽),就需要你自己做取舍,做强制转换;
  • 如果相互之间没办法转换,就无法使用;
  • 每个方法都有自己相对独立的变量空间,和其他方法各自独立。

返回值

  • 如果有声明方法的返回值类型,那么就必须用return返回对应类型的值,这个值可以是基本类型,也可以是某个对象类型;
  • 一个方法可以声明多个return,但如果遇到其中一个,就会提前结束方法的执行。
  • 即使是返回void的方法,也可以使用return来提前结束当前方法。

本地变量

  • 方法每次被调用,计算机都会为它分配一片独立的内存区域,作用域只在这个方法里面,里面的变量,称为本地变量。

    • 作用域:顾名思义,就是能够起作用的范围是哪些区域。本地变量的作用域也就是大括号里面;
    • 每个变量也都会有自己的生存期,在被创建的时候诞生,在作用域消失的时候就没了;
    • 进入到某个作用域之前,里面的变量是不会被创建的。
  • 参数列表、方法里新定义的都是本地变量;

  • 不同作用域的本地变量,可以重名,不冲突。但是一个大括号里面的下一级出现同名就会出现冲突。

  • 本地变量不会被自动初始化,但是在使用前必须被初始化。

  • 可以用class关键字来修饰一个类;
  • 我们通常会把各种事物分解成各种属性和功能,再抽象成类的形式。
public class Bus {
    int width;
    int height;
    int seating;
    double price;
    double balance;
    String number;
    boolean isOpen;
    boolean isRuning;


    public void stop() {
        isRuning = false;
        System.out.println("已停车!");
    }

    public void start() {
        isRuning = true;
        System.out.println("正在行驶!");
    }

    public void regist(String no) {
        number = no;
        System.out.println("注册成功!您的车牌号是" + number);
    }

    public void pay(double money) {
        if (money < price) {
            System.out.println("钱没给够,快下车!");
        } else {
            balance = balance + money;
            seating -= 1;
            System.out.println("欢迎乘坐!");
        }
    }

    public void openDoor() {
        isOpen = true;
        System.out.println("开门!");
    }

    public void closeDoor() {
        isOpen = false;
        System.out.println("关门!");
    }
}

类的使用

  Bus b = new Bus();
  b.regist("闽A000000");
  b.start();
  b.stop();
  b.openDoor();
  b.pay(0);
  b.start();
  • 使用new关键字,可以把类实例化(创建)成一个对象,并交给变量b来持有;
  • 通过b.可以访问这个对象里面的方法或者成员变量

成员变量

成员变量:定义在类内部但在方法外部的变量,通常用于存储与对象状态相关的信息。成员变量可以是任何数据类型,包括基本数据类型(比如int、double、char等)或引用类型(比如其他类的对象、数组等):

  • 定义在类里面的大括号下的变量,叫成员变量;

  • 成员变量会有一个默认的值;

  • 每个对象内的成员变量是相互独立的

    1. 所以需要使用对应的对象名(b.)来访问。
    2. 如果是在类里面的方法访问成员变量,可以用this关键字,或者是直接用变量名访问。
    3. 被static修饰过的方法,叫做静态方法,静态方法不属于任何一个实例对象。所以,不能在静态方法里面直接访问成员变量。

构造方法

构造方法(也称为构造函数)是一种特殊类型的方法,用于初始化新创建的对象。构造方法的名字必须与类名完全相同,并且不能有返回类型(甚至不能是void)。当你创建一个新对象时,Java会自动调用这个类的构造方法:

  • 每个类都可以有多个构造方法,他们有不同的参数列表(重载);
  • 名称需要和类名完全相同(包括大小写也要一致);
  • 构造方法没有返回类型,即使没有写出void声明;
  • 构造方法可以有参数,也可以没参数;
  • 不能被直接调用,必须通过new关键字创建对象的时候自动调用。

类似于文件夹,使用包来管理代码、存放代码,可以提升代码可读性、维护性。

  1. 不同的包,允许有重名的文件;
  2. 包可以有多个层级;
  3. 不同的包相互引用需要关注权限修饰符;
  4. 不同的包相互引用,需要使用import;
  5. 分包建议使用3级目录,比如“com.ls.test”,所有的代码至少写在test下面,如果还需要继续分包,一般也会在test下面继续分:
    1. com:第一级别用com、cn这样的域名结尾;
    2. ls:第二级一般用作者名、公司名;
    3. test:第三级指的是当前这个项目具体的项目名称。

权限修饰符

可以对成员变量和成员方法做修饰,以此来控制操作权限:

  1. public:所有人可访问
  2. private:当前类可以访问
  3. 不写权限修饰符:一个包下可以访问
  4. protected:一个包下面可以访问,不同包下面的子类可访问

静态变量、静态方法,(类变量、类方法)

static修饰过的成员变量或者成员方法,就叫静态变量(类变量)、静态方法(类方法),因为他是属于类本身的,而不是属于任何一个被实例的对象:

  • 想要访问静态变量、静态方法,可以通过类名直接访问,也可以通过某一个被实例的对象访问;
  • 静态变量、静态方法他是所有实例对象共同的变量(方法);
  • 静态变量只会在类被加载的时候被初始化一次;
  • 静态方法里不能直接访问成员变量、成员方法,因为成员变量和方法属于具体的对象。

容器

用来存放一串数据的东西可以被称为容器,java中除了数组,还有其他对象类型的容器。我们可以根据应用场景来选择合适的容器。

ArrayList

  1. 相对于数组的固定长度来说,ArrayList可以根据数据的数量,动态扩展自身长度。理论上来说,只要计算机内存够,可以无限扩展;
  2. ArrayList比较灵活,可以通过index(下标索引)直接查询、删除、插入数据到某个指定位置。
//定义一个ArrayList
ArrayList<String> student = new ArrayList<String>();

//添加一个数据
student.add("小李");
//往指定位置添加一个数据,0指的是需要添加位置的下标
//这个方式会让原有数据往后移,为新的这条数据腾出位置
student.add(0, "老张");

//获取指定位置的数据,如果超出容器本身的长度,会报数组下标越界异常
student.get(2)

//删除指定位置的数据,如果删除成功会返回这条数据
String string = student.remove(5);

注意:更多的用法请自行参阅java api,从现在起,培养阅读文档的好习惯!

HashSet

  1. HashSet中不允许存在两个相同的元素,新插入的数据会替换之前的数据;
  2. HashSet没有下标索引这一说法,因为它对位置没有概念,只知道自己的内部存放了哪些数据。
//定义一个HashSet
HashSet<String> set = new HashSet<String>();

//往里面存数据
set.add("老孙");

//将HashSet转为数组,再通过数组的形式处理里面的元素
String[] strs = set.toArray(new String[0]);
for (int i = 0; i < strs.length; i++) {
    System.out.println(strs[i]);
}

//获取HashSet的迭代器,使用迭代器对象来循环遍历处理里面的元素
Iterator<String> iterator = set.iterator();
//hashNext方法会返回一个布尔值,用来判断是否还有下一个元素可以被遍历,如果有则返回true。如果已经到了末尾,则返回false
while (iterator.hasNext()) {
	//使用next获取下一个元素
    String next = iterator.next();
    System.out.println(next);
}

关于容器的遍历

主要有几种形式,可以根据应用场景和需求来选择:

//第一种:for循环,如果需要处理位置索引,用这种比较合适
for (int i = 0; i < student.size(); i++) {
   System.out.println("第" + (i + 1) + "位是" + student.get(i));
}

//第二种:for each循环,如果只是单纯的循环遍历,可以用这种
for (String str : student) {
   System.out.println(str);
}

//第三种:如果只是查看里面的数据,可以直接打印容器对象
System.out.println(student);

//第四种:使用容器的迭代器对象,list、set都可以获取到iterator
Iterator<String> iterator = set.iterator();

HashMap

  1. 和HashSet类似,HashMap只允许存在1个唯一的Key。新添加的Key如果已存在,会把旧的元素替换。
//定义一个HashMap
//K:key查询的关键值  V:value具体的数据
HashMap<String, Student> hashMap = new HashMap<>();

//往里面添加一条数据   第一个参数需要指定key值,第二个参数指定数据
hashMap.put(laosun.name, laosun);

//通过key查询数据,返回对应的数据类型
Student query = hashMap.get(s);

//获取hashmap的长度
int length = hashMap.size();

//获取hashmap中所有key的一个集合
Set<String> keySet = hashMap.keySet();

//通过key的集合遍历hashmap
for (String key : keySet) {
	Student stu = hashMap.get(key);
}

继承

继承是面向对象里面很重要的概念,继承允许我们创建分等级层次的类。

//父类
public class People {

    int age;
    private String name;
    int gender;  //1表示男生 0表示女生
}

//子类
public class Student extends People {
    int number;
}
  • 被继承的类叫做父类,使用extends的叫做子类。

  • 我们通常可以把很多重复的属性、方法放到一个父类当中,让子类继承父类的能力,可以提升代码的维护性、扩展性。

  • Java支持多层级继承,比如 LaoSun 继承了 Student,Student继承了People;

  • 可以使用super关键字访问父类的方法或者属性:

//访问父类的构造方法,注意:需要写在子类构造方法的第一行
super(age, name, gender);
//访问父类的成员变量
super.name;
//访问父类的方法
super.getName();
  • 如果父类有重载构造方法,那么在子类构造方法里也需要指定父类的构造方法是哪个
public class People {

	//父类中重载了构造方法
    public People(int age, String name, int gender) {
        this.age = age;
        this.name = name;
        this.gender = gender;
    }

}

public class Student extends People {

    public Student(int number, int age, String name, int gender) {
		//子类中需要指定父类的构造方法
        super(age, name, gender);
        this.number = number;
    }
}
  • 如果父类和子类的属性、方法冲突,子类会去找离他最近的那个属性或者方法。

  • 子类可以拥有父类的非private方法和属性

子类对象可以被当成是父类对象来使用,比如说:

//被放到存放父类型的容器
ArrayList<People> peoples = new ArrayList<>();
Student laosun = new Student(10, 18, "老孙", 1);
peoples.add(laosun);

//被赋值给父类变量,注意:这种方式被称为向上转型
People laoli = new Student(13, 21, "老李", 0);

//同时,也有向下转型:把父类变量强制转为子类变量
//前提是 laoli 这个对象赋值的时候就是 Student, 否则无法强转
Student stu = (Student) laoli;

//被传递到需要父类类型的方法中
public void addPeople(People p1){
	peoples.add(p1);
}

注意:向下转型能否成功,取决于对象的实际运行时类型,而不是引用变量的声明类型。

多态

调用的父类的show,但实际执行,Java却知道分别是属于哪个子类,从而调用对应的show

public class People {   

	public String show() {
        return null;
    }

}

public class Student extends People {
	//子类重写父类方法
    @Override
    public String show() {
        String name = getName();
        return "姓名:" + name + ",学号是" + number + ",年龄" + age + ",性别是" + gender;
    }
}

People laoli = new Student(13, 21, "老李", 0);
//这里实际调用的是子类的show方法
System.out.println("你要查询的学生信息:" + laoli.show());

重写

要成功地重写父类中的方法,需要满足以下条件:

  1. 方法名称、参数列表必须相同:子类重写父类的方法时,方法名称和参数列表(包括参数类型、参数顺序和参数数量)必须与父类中被重写的方法完全相同。
  2. 访问权限不能更低:子类重写父类的方法时,访问权限(public、protected、默认(无修饰符)或private)不能低于父类中被重写的方法的访问权限。例如,如果父类中的方法是protected的,那么子类中的重写方法不能是private的。
  3. 返回类型要兼容:子类重写父类的方法时,返回类型必须与父类中被重写的方法的返回类型相同或是其子类。在Java 5及以后的版本中,如果父类中的方法返回类型是泛型,子类重写的方法可以返回该泛型的子类。
  4. 子类重写的方法不能抛出比父类方法更广泛的检查型异常:检查型异常是编译器在编译时可以检查出来的异常。子类重写父类的方法时,可以抛出与父类方法相同的检查型异常,或者是父类方法抛出的检查型异常的子类。但是,子类重写的方法不能抛出新的检查型异常,除非这个异常是运行时异常(RuntimeException)或其子类。
  5. 父类中的方法不能为final:如果父类中的方法被声明为final,那么子类不能重写该方法。
  6. 父类中的方法不能为static:如果父类中的方法是静态的(static),那么子类不能重写该方法。但是,子类可以定义与父类静态方法相同名称和参数列表的静态方法,这被称为隐藏(Hiding),而不是重写。

Object类

Object是所有对象类型的父类,比如自己定义的Student、Pepole,或者JDK提供的String、Scanner等等。

判断两个对象是否相等

不可以直接使用equals或者==来判断,而要重写equals,自己定义判断条件:

    @Override
    public boolean equals(Object obj) {
		//判断 obj是不是Student类型的对象
        if (obj instanceof Student) {
            Student stu = (Student) obj;
            return number == stu.number && getName().equals(stu.getName());
        } else {
            return false;
        }

    }

	    Student stu1 = new Student(10, 18, "老孙", 1);
        Student stu2 = new Student(10, 18, "老孙", 1);
		//使用重写后的equals判断两个条件是否相等
        if (stu1.equals(stu2)) {
            System.out.println("stu1等于stu2!");
        } else {
            System.out.println("stu1不等于stu2!");
        }

抽象类

抽象类可以为某一类对象定义基础的框架结构

使用抽象类需要注意的

  • 抽象类不能被实例化;
  • 抽象类可以包含抽象方法和非抽象方法;
  • 如果类里面包含抽象方法,那么这个类也必须是抽象的;
  • 子类必须实现父类中的所有抽象方法,除非子类本身也是抽象的;

为什么要使用抽象

  • 代码重用:通过继承抽象类,子类可以重用父类中定义的属性和方法,减少了代码的重复;
  • 约束和强制:抽象类可以定义一组子类必须实现的方法,从而强制子类遵循约定实现特定的方法;
  • 扩展性:抽象类允许在保持已有代码稳定的同时,通过添加新的子类来扩展系统的功能。
  • 多态性:子类继承了父类,所以可以使用父类类型的变量持有子类对象,实现运行时多态。
  • 模板方法模式:抽象类可以作为一种模板,提供算法的框架和一些默认实现,让子类在不改变算法结构的情况下,重新定义某些步骤的具体实现;
  • 如果是一些简单的类,可以不需要抽象。
//定义一个抽象类
public abstract class Animal {

    public abstract void eat();

    public void makeSound() {

    }
}

//继承一个抽象类
public class Dog extends Animal {

    @Override
    public void eat() {
        System.out.println("我是一只狗子,我正在吃骨头");
    }

    @Override
    public void makeSound() {
        System.out.println("我是一只狗子,我会汪汪汪");
    }

}

接口

接口是一种特殊的抽象类,接口中的所有成员方法都是抽象方法,所有成员变量都是public static final:

  • 使用implements关键字可以实现接口,但必须强制重写所有方法;
  • 可以不需要使用public abstract修饰方法,并且权限只能是public;
  • 可以不需要使用public static final修饰成员变量;
  • 接口不能直接被实例化,也没有构造方法;
  • 接口不可以实现另一个接口,但是接口可以支持多继承的关系;
  • 在Java8以后可以使用default或者staic在接口中实现方法体,Java9后可以使用private。
//定义一个接口
public interface Animal {

    public static final int a = 1;

    void sleep();

    void eat();

    //Java8以后可以用
    default void game() {

        System.out.println("来打游戏");
    }

    //Java8以后可以用
    static void run() {
        System.out.println("动物是可以跑的!");
    }

    //Java9以后可以用 只能在接口Animal内部使用
    private void test() {

    }
}


//使用一个接口
public class Dog implements Animal {
    @Override
    public void sleep() {
        System.out.println("狗子在睡觉");
    }

    @Override
    public void eat() {
        System.out.println("狗子在吃饭");
    }
}

内部类

定义在另一个类内部的类,称为内部类。内部类可以访问外部类的所有成员(包括私有成员),而外部类不能直接访问内部类的私有成员,只能通过内部类的对象来访问。

成员内部类

  • 和外部类的成员变量、成员方法处于同一个级别上;
  • 创建成员内部类的对象时,需要借助外部类的对象;
        Restaurant restaurant = new Restaurant();

        //通过一个Restaurant的示例直接访问成员内部类  需要内部类是public
//        Restaurant.MenuItem menuItem = restaurant.new MenuItem("扁肉", 5);

        //访问一个private的成员内部类
        restaurant.createMenuItem("扁肉", 5);

        restaurant.showSpicy("特别辣");

静态内部类

  • 静态内部类使用static关键字定义,它属于外部类本身,而不属于外部类的某个实例;
  • 创建静态内部类的对象时,不需要外部类的对象作为参数;
 //需要static修饰MenuItem才能访问 需要内部类是public
        Restaurant.MenuItem menuItem = new Restaurant.MenuItem("拌面", 5);

局部内部类

  • 局部内部类定义在外部类的方法中;
  • 局部内部类的作用域仅限于该方法内;
  • 局部内部类不可以有访问修饰符和static修饰符。
public void showSpicy(String spicy) {

	class LocalClass {
		public void display() {
			System.out.println("辣度是:" + spicy);
		}

	}

	LocalClass localClass = new LocalClass();
	localClass.display();
}

//在外部实例化类,再调用showSpicy方法
restaurant.showSpicy("特别辣");

匿名内部类

  • 匿名内部类是没有名字的内部类;
  • 匿名内部类通常用于实现接口或继承其他类,并立即创建该类的对象;
  • 常用于线程、事件监听器等场景。

内部类的常见场景

  1. 内部类可以将一组紧密相关的类组织在一起,形成一个逻辑上的单元,从而提高了代码的可读性和可维护性;
  2. 内部类可以隐藏不需要被外部类直接访问的实现细节。通过把内部类设为私有(private),可以确保它不会被外部类直接访问,只能通过外部类提供的方法来间接访问;
  3. 使用内部类可以简化代码结构;

异常处理

在程序当中,经常会出现一些异常与错误,Java为了我们能够更好的处理异常,也提供了异常处理机制,这可以让代码变得更加健壮:

异常机制的使用方式

//尝试步骤一个异常
try {
    // 可能会抛出异常的代码
} catch (IndexOutOfBoundsException e) {
    // 处理异常的代码
}finally{
  // 不管会不会发生异常,这里的代码都会被执行
}

//尝试在try中捕捉多个异常
try{
  // 可能会抛出异常的代码
}catch(异常类型 异常变量名){
  // 处理异常的代码
}catch(异常类型 异常的变量名){
 // 处理异常的代码
}catch(异常类型 异常的变量名){
  // 处理异常的代码
}

//抛出自己的异常
public void check(int i) {
  if (i < 0) {
    throw new IllegalArgumentException("传入了1个小于0的数");
  }
}

//将异常传递到外部
public void readFile(String filePath) throws IOException,NullPointerException {
 
}

检查型异常和运行时异常

  • 运行时异常不会强制要求使用try catch捕捉,也不强制要求需要throws
  • 检查型异常要求需要try catch,也要求throws

声明自定义异常

public class StringLengthException extends Exception{

}

异常在哪里使用

  • 根据自身业务场景的需要,在容易出现问题的地方做异常处理,可以增强稳定性、可靠性;
  • 尽量不要过度使用异常,而是尽量精准的使用;
  • 善于使用finally释放资源,例如在finally里面做一些资源释放。

常见的异常类型

异常 描述
ArithmeticException 当出现异常的运算条件时,抛出此异常。例如,一个整数"除以零"时,抛出此类的一个实例。
ArrayIndexOutOfBoundsException 用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引。
ArrayStoreException 试图将错误类型的对象存储到一个对象数组时抛出的异常。
ClassCastException 当试图将对象强制转换为不是实例的子类时,抛出该异常。
IllegalArgumentException 抛出的异常表明向方法传递了一个不合法或不正确的参数。
IllegalMonitorStateException 抛出的异常表明某一线程已经试图等待对象的监视器,或者试图通知其他正在等待对象的监视器而本身没有指定监视器的线程。
IllegalStateException 在非法或不适当的时间调用方法时产生的信号。换句话说,即 Java 环境或 Java 应用程序没有处于请求操作所要求的适当状态下。
IllegalThreadStateException 线程没有处于请求操作所要求的适当状态时抛出的异常。
IndexOutOfBoundsException 指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。
NegativeArraySizeException 如果应用程序试图创建大小为负的数组,则抛出该异常。
NullPointerException 当应用程序试图在需要对象的地方使用 null 时,抛出该异常
NumberFormatException 当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。
SecurityException 由安全管理器抛出的异常,指示存在安全侵犯。
StringIndexOutOfBoundsException 此异常由 String 方法抛出,指示索引或者为负,或者超出字符串的大小。
UnsupportedOperationException 当不支持请求的操作时,抛出该异常。
异常 描述
ClassNotFoundException 应用程序试图加载类时,找不到相应的类,抛出该异常。
CloneNotSupportedException 当调用 Object 类中的 clone 方法克隆对象,但该对象的类无法实现 Cloneable 接口时,抛出该异常。
IllegalAccessException 拒绝访问一个类的时候,抛出该异常。
InstantiationException 当试图使用 Class 类中的 newInstance 方法创建一个类的实例,而指定的类对象因为是一个接口或是一个抽象类而无法实例化时,抛出该异常。
InterruptedException 一个线程被另一个线程中断,抛出该异常。
NoSuchFieldException 请求的变量不存在
NoSuchMethodException 请求的方法不存在

文件读写

每一个程序都需要输入和输出功能,除了面向控制台的输入、输出,Java还提供了面向与本地文件的读写操作。

写入

这个过程就是借助FileOutputStream,把内存当中hello里的内容输出到文件。

    public static void main(String[] args) {

		//需要写入的内容
        String hello = "hello world!";
		//需要写入的文件路径,如果只写文件名,那就默认是当前工程的根目录
        String filePath = "text.txt";
		//创建一个FileOutputStream对象,用于处理文件的写入操作
        try (FileOutputStream fileOutputStream = new FileOutputStream(filePath);) {
			//将hello转为byte数组,以便fos处理写入操作
            byte[] bytes = hello.getBytes();
			//调用写入方法,写入文件
            fileOutputStream.write(bytes);

            System.out.println("文件写入成功:" + filePath);

        } catch (FileNotFoundException e) {
            System.out.println("异常:找不到文件!");
        } catch (IOException e) {
            System.out.println("异常:文件写入失败!");
        }
		
//        finally {
//            try {
//                fileOutputStream.close();
//            } catch (IOException e) {
//                throw new RuntimeException(e);
//            }
//        }
    }

读取文件

读取的操作实际上就是借助FileInputStream,把文件读取到程序的内存当中。

  public static void main(String[] args) {
		
		//需要读取的文件路径,如果只写文件名,那就默认是当前工程的根目录
        String filePath = "D:\\ls course\\【下载必看】Java相关资料说明.txt";
		//创建一个FileInputStream对象,用于处理文件的写入操作
		//再创建一个InputStreamReader来封装fis,isr可以帮助我们处理文本信息的编码、以及一些细节
        try (FileInputStream fis = new FileInputStream(filePath);
			//创建isr时顺便指定编码格式,在国内一般都是UTF-8的编码集
             InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8);
        ) {
			//创建一个临时的缓冲区,每次读写1024的内容长度
            char[] bytes = new char[1024];
			//创建一个StringBuilder,在每一次读取后,都往sb中存入读取的数据
            StringBuilder sb = new StringBuilder();
			//用于记录每次读取的实际长度
            int charLength;
			//在循环中使用read读取文件,如果已经没有新的数据可以读,就会返回-1,跳出循环
            while ((charLength = isr.read(bytes)) != -1) {
				//将每次读取的内容存入sb,并且指定每次存入的数据长度
                sb.append(bytes, 0, charLength);
            }
			//将sb转为字符串对象
            String string = sb.toString();
            System.out.println("读取到的内容是:" + string);

        } catch (IOException e) {
            System.out.println("文件操作异常");
        }

    }

“流”(Stream)是一个抽象的概念,用于处理数据序列。Java中的流主要有两种类型:

输入输出流(Input、Output)

  • I/O流用于读取(输入)和写入(输出)数据,
  • 它们通常与数据源交互,比如网络传输、文件、控制台等等数据交互;
  • 常见的I/O流有FileInputStream, FileOutputStream, BufferedReader, BufferedWriter
  • I/O流是Java I/O库的一部分,它提供了丰富的类和方法来处理各种输入和输出任务,我们只需要在合适的场景选择对应的流即可。

Stream API 函数式编程流(感兴趣的可以自行扩展)

  • JDK8中,允许我们以声明的方式处理数据集合(如List、Set等),可以提高代码的可读性和可维护性;
  • Stream API基于函数式编程的概念,提供了一系列针对于容器、集合的map、filter、reduce等操作方法,可以在不修改原始数据集合的情况下,对里面的数据进行转换、过滤和聚合等操作,可以提升编程效率;
  • 与传统的for-each循环相比,Stream API通常能写出更简洁、更易于理解的代码;
  • Stream API和IO流不一样,Stream API主要用于处理内存中的数据集合,而不是与数据源直接交互。
//简单举个例子:使用Stream API,在不改变numbers这个字符串列表的前提下,使用里面的数据,直接转成int并求和
List<String> numbers = Arrays.asList("1", "2", "3", "4", "5");  
int sum = numbers.stream()  
                  .mapToInt(Integer::parseInt)  
                  .sum();  
System.out.println(sum); // 输出 15

泛型

使用泛型,可以编写灵活的、可重用的组件(类、接口、方法),这些组件可以处理多种类型,而不必为每种数据类型编写单独的代码。

//在类中使用泛型
public class Box<T> {

    private T data;
    private Printable<T> printable;


    public Box(Printable<T> printable) {
        this.printable = printable;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public void printData() {
        printable.print(data);
    }
}

//定义泛型接口
public interface Printable<T> {
    void print(T data);
}

//实现接口
public class IntegerPrintable implements Printable<Integer> {

    @Override
    public void print(Integer data) {
        System.out.println("这是一个int类型,值是:" + data);
    }
}

public class StringPrintable implements Printable<String> {

    @Override
    public void print(String data) {
        System.out.println("这是一个String类型,值是:" + data);
    }
}


//使用Box处理不同类型的数据
 Box<Integer> boxInteger = new Box<>(new IntegerPrintable());
 boxInteger.setData(9);
 boxInteger.printData();

 Box<String> boxString = new Box<>(new StringPrintable());
 boxString.setData("hello world");
 boxString.printData();

//在方法中使用泛型
public static <T> void printList(ArrayList<T> list) {
	for (T data : list) {
	System.out.println("数据 = " + data);
	}
}

为什么要使用泛型

  1. 类型安全:泛型提供了编译时的类型检查,可以减少由于类型不匹配而导致的运行时错误;
  2. 代码重用:泛型编写的代码更加灵活、可重用,因为我们使用相同的类、接口或方法来处理多种数据类型;
  3. 减少装箱和拆箱:对于基本数据类型和包装类之间的转换,泛型可以减少性能开销。

进程与线程

  • 多数情况下,一个进程可以理解为对应一个程序,但是随着计算机的发展,一个应用有可能会有多个进程(多个应用也可能只共享一个进程。比如我们经常听到的微服务、分布式,就有可能是一个程序对应多个进程,但这些目前不是我们需要关心的。我们只要知道:进程是操作系统分配资源的基本单位;
  • 1个进程可以有多个线程,线程是操作系统实现任务调度的基本单位,程序里的各种任务就是由一个个线程来实现的任务调度;
  • 每一进程都有属于自己的存储空间和系统资源,而同一进程下的线程可以共享这个进程的内存与资源;
  • 在C C++这种相对底层的语言,会直接提供进程、线程的操作方法,但Java通常只需要关心线程的使用;

线程的创建

继承Thread

如果只是简单的需要开启一个线程做操作,那使用这种方式即可。

public class MyThread extends Thread {  
    public void run() {  
        // 线程执行的代码  
        System.out.println("MyThread is running");  
    }  
 
    public static void main(String[] args) {  
        MyThread thread = new MyThread();  
        thread.start(); // 启动线程  
    }  
}

实现Runnable接口

这种方式可以允许多个线程共享1个runnable实例,好处是可以让线程间共享数据和代码。

public class MyRunnable implements Runnable {  
    public void run() {  
        // 线程执行的代码  
        System.out.println("MyRunnable is running");  
    }  
 
    public static void main(String[] args) {  
        Thread thread = new Thread(new MyRunnable());  
        thread.start(); // 启动线程  

		//这种写法默认是在主线程运行(主线程:启动Java应用程序的线程)
        myRunnable.run();
    }  
}

使用Callable

//实现callable接口
public class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {

        System.out.println("call 开始了");
        Thread.sleep(3000);

        int a = 10;
        int b = 20;
        int c = a + b;
        return c;
    }
}

//在main方法中使用
    /**
     * Callable的用法
     *
     * @param args
     */
    public static void main(String[] args) {

//        MyCallable callable = new MyCallable();
		//或者写成匿名内部类的形式
        Callable<Integer> callable = new Callable<>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("call 开始了");
                Thread.sleep(3000);
                int a = 10;
                int b = 20;
                int c = a + b;
                return c;
            }
        };
		//借助FutureTask包装callable
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
		//在新的线程中使用callable
        Thread thread = new Thread(futureTask);
        thread.start();
		
		//取消当前线程的执行
        //futureTask.cancel(true);
        try {
			//使用get获取结果,注意:这会调用get的线程被阻塞等待结果返回
            int integer = futureTask.get();
            System.out.println("int = " + integer);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

三种方式怎么选择?

  • 如果只是单纯的想要开辟一个线程做一个事情,可以直接使用Thread;
  • 如果想要多个线程共同操作一些数据,可以使用Runnable;
  • 如果需要线程处理完返回结果,可以使用callable。

为什么要使用多线程编程

  1. 充分利用多核处理器,提升程序的性能
  2. 避免主线程阻塞等待,提升用户体验

Thread的常用操作

//获取当前的线程
Thread.currentThread();

//让当前线程睡眠指定时间,单位是毫秒
Thread.sleep(3000);

//当前线程等待指定时间,单位是毫秒
Thrad.waite(3000);

//线程愿意放弃其当前对处理器的使用
Thread.yield();

========以下是通过对象调用的常见操作========

//获取线程的id和名字
thread.getId();
thread.getName();

//设置线程的名字
thread.setName("thread1");

//设置线程的优先级为最大
thread.setPriority(Thread.MAX_PRIORITY);

//开始线程执行,这个方法会让run方法被触发
 thread.start();;

如何让Thread停止操作

  1. 自己定义一个状态来控制;
  2. 使用interrupt()方法:如果当前线程正被阻塞(sleep、wait)的时候被interrupt,那么就会触发InterruptedException异常,通常我们会在异常代码块中处理资源的释放,需要注意,抛出InterruptedException异常后,中断的状态会被清除。另外,如果当前线程没有被阻塞,那么就会正常中断线程操作;
  3. 如果是Callable的形式,可以使用futureTask.cancel(true);

线程安全

当多个线程同时访问共享资源时,可能会导致数据不一致、重复等问题。Java也提供了解决线程安全的方案:

Synchronized关键字

Synchronized同步锁,解决了多线程的数据安全问题。可当线程很多时,因为每个线程都会判断其他线程是否是有锁,这是很耗费资源的,会降低程序的运行效率。

成员同步方法

  • 这种方式的锁对象指的就是:this
public synchronized void sellTicket() {
        while (ticket > 0) {
            ticket--;
            System.out.println("卖了一张票,这张票的序号是:" + ticket + " 库存剩余:" + ticket);
        }
}

同步代码块

  • 这时可以指定任何对象作为锁。这通常用于更细粒度的控制.
public void sellTicket() {
	//指定获取this的所锁
	synchronized (this) {
		while (ticket > 0) {
			ticket--;
			System.out.println("卖了一张票,这张票的序号是:" + ticket + " 库存剩余:" + ticket);
		}
	}
 }

public class MyClass {  
    private final Object lock = new Object();  

    public void myMethod() {  
        synchronized (lock) {  
            // 锁定的是lock对象  
        }  
    }  
}

同步静态方法

  • 这种形式的锁对象是这个类的class对象
public static void Test() {

	synchronized (MovieTicket.class) {

	}
}

public static synchronized void Test() {

}

Lock

Lock是一个接口,也可以用来解决线程同步问题,我们可以使用实现了Lock接口的ReentrantLock,可以比synchronized更精细的控制锁,并且还可以做很多扩展操作。

    private int ticket = 100;//表示有100张电影票
    private final ReentrantLock reentrantLock = new ReentrantLock();

    public void sellTicket1() {
        while (ticket > 0) {
			//上锁
            reentrantLock.lock();
            try {
                if (ticket > 0) {
                    ticket--;
                    System.out.println("卖了一张票,这张票的序号是:" + ticket + " 库存剩余:" + ticket);
                } else {
                    System.out.println("在其他窗口卖完了 库存剩余:" + ticket);
                }
            } finally {
				//不管是否发生异常,都执行解锁操作
                reentrantLock.unlock();
            }

        }

    }

两种形式如何选择?

  • 多数简单的场景下使用synchronized,因为JDK6以后,synchronized的性能和lock不相上下;
  • 需要做更多复杂的功能,就使用lock。

死锁

每个线程都在等待其他线程释放资源,从而导致它们都无法继续执行。死锁通常发生在多线程环境中,当两个或更多的线程无限期地等待一个系统资源(如内存锁),而每个线程又在等待被另一个线程持有的资源时,就会发生死锁。

常见的死锁场景:

  • 嵌套锁:当一个线程获取了一个锁,然后又试图获取第二个锁,而第二个锁已经被另一个线程持有,同时这个线程又持有第一个线程需要的锁;
  • 锁顺序不一致:当多个线程尝试以不同的顺序获取多个锁时,可能会发生死锁;
  • 可重入锁的不当使用:如果一个线程在没有释放锁的情况下重复请求同一个锁,而其他线程正在等待这个锁,那么也可能导致死锁;
  • 超时等待:如果线程在尝试获取锁时设置了超时,并且超时时间太短,那么它可能在等待其他线程释放锁之前就放弃了,这可能导致死锁,特别是当多个线程都这样做时。

如何避免死锁

  1. 避免嵌套锁:尽量确保线程按照一致的顺序获取锁;
  2. 使用锁超时:为线程尝试获取锁设置超时时间,以避免无限期等待;
  3. 使用并发库:目前我们虽然只学过Synchronized和Lock,但Java还提供了许多高级同步工具,如CountDownLatchCyclicBarrierSemaphoreExchanger等,这些工具可以帮助减少死锁的风险,后续也会陆续接触到。

wait与notify

notifynotifyAllwait 是 Java 中用于多线程间通信的方法,它们都属于 Object 类的方法,用于在对象的监视器(monitor)上等待或通知线程。而 sleepThread 类的一个静态方法,用于让当前线程暂停执行一段时间。

注意:notifynotifyAllwait 都只能在同步代码快或者同步方法里被使用,意思就是他们必须在拥有某个对象所的线程里被调用。

wait

当线程需要等待某个条件成立时,可以调用 wait 方法将自己挂起,并释放当前持有的监视器锁。当其他线程改变了条件并调用 notifynotifyAll 时,被挂起的线程可能会被唤醒。

notifynotifyAll

多个线程需要等待某个条件成立才能继续执行时,可以使用 wait 将线程挂起,并在条件成立时使用 notifynotifyAll 唤醒等待的线程。这种做法可以让线程之间能够基于某个条件进行同步。

注意:notify 只会随机唤醒一个等待在该对象上的线程,而 notifyAll 会唤醒所有等待在该对象上的线程。

wait和sleep

  • 如果你需要在多线程之间进行基于某个条件的同步和通信,使用 waitnotifynotifyAll
  • 如果你只是想让当前线程暂停执行一段时间,而不涉及与其他线程的同步或通信,使用 sleep

线程的生命周期

在这里插入图片描述

线程的6种状态

  1. 新建状态(NEW)

    • 当一个线程对象被创建,但尚未调用其start()方法时,该线程处于新建状态。此时,线程对象已经存在,但系统并未为其分配资源,因此它还没有开始执行。
  2. 就绪状态(RUNNABLE)

    • 当线程调用start()方法后,它进入就绪状态。这意味着线程已经准备好运行,但是否真正执行取决于操作系统的调度。就绪状态包括了“就绪”和“运行”两种细分状态:

      • 就绪:线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权。
      • 运行:当线程获得CPU资源并执行任务时,它处于运行中状态,正在执行其run()方法中的代码。
  3. 阻塞状态(BLOCKED)

    • 当线程等待获取一个锁以进入或重新进入同步代码块时,它进入阻塞状态。这意味着线程正在等待一个资源或条件,以便能够继续执行。
  4. 等待状态(WAITING)

    • 当线程被另一个线程所阻塞,等待某个条件成立或获得某个对象的监视器锁时,它处于等待状态。例如,当线程调用无参数的wait()方法时,它会进入等待状态。在这种情况下,线程会等待另一个线程的操作完成或者等待notify/notifyAll消息。
  5. 定时等待状态(TIMED_WAITING)

    • 当线程等待另一个线程执行特定操作或等待指定时间后继续执行时,它处于定时等待状态。这通常通过调用Thread类的sleep()方法或使用java.util.concurrent包中的工具类来实现。在指定的时间过去之后,线程会自动返回就绪状态。
  6. 终止状态(TERMINATED)

    • 当线程完成执行任务或因异常终止时,它处于终止状态。此时,线程已经不再运行,并且无法再次被启动。线程的消亡是不可逆的,不能对处于终止状态的线程再次执行start()方法。

这6种状态定义在Thread类的State枚举中,并且可以通过调用线程的getState()方法来获取当前线程的状态。这些状态涵盖了线程从创建到消亡的整个生命周期,并且是线程调度和同步机制的基础。

线程池

线程池是Java对线程的一种管理模式,它可以预先定义一定数量的线程,允许我们通过复用已有的线程来处理任务,而不需要为每个任务都创建一个新的线程。这减少了线程创建和销毁的开销,提高了系统的效率和稳定性。

优点

  • 降低资源消耗:通过复用线程,减少了线程创建和销毁的开销。
  • 提高响应速度:当任务到达时,可以立即从线程池中获取线程执行任务,而不需要等待线程的创建。
  • 提高线程的可管理性:线程池允许我们开启多个任务而不用为每个线程设置属性值,便于管理和调优。

使用场景

  • 高并发场景:当有大量任务需要并发执行时,使用线程池可以避免频繁创建和销毁线程的开销。
  • 任务执行时间短:对于执行时间较短的任务,使用线程池可以提高系统的吞吐量。
  • 需要控制最大并发数:通过线程池可以限制同时运行的线程数,从而避免系统资源的过度消耗。

线程池的使用

Java提供了多种类型的线程池,newFixedThreadPool(固定线程数线程池)、newSingleThreadExecutor(单线程化线程池)、newCachedThreadPool(可缓存线程池)等,它们适用于不同的使用场景。

newFixedThreadPool

newFixedThreadPool可以指定固定大小的线程池,可以限制同时执行的线程数量,防止系统资源耗尽。

public class FixedThreadPoolExample {  
    public static void main(String[] args) {  
       int corePoolSize = 10; // 核心线程数
        //创建一个固定大小的线程池
        ExecutorService executor = Executors.newFixedThreadPool(corePoolSize);

        // 提交任务到线程池
        for (int i = 0; i < 20; i++) {
            final int taskId = i;
			//提交一个任务
            executor.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("Task " + taskId + " is running by thread " + Thread.currentThread().getName());
                    // 模拟任务执行时间
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        // 关闭线程池
        executor.shutdown();
        while (!executor.isTerminated()) {
            // 等待所有任务完成
        }

        System.out.println("All tasks are completed.");
    }  
}

newSingleThreadExecutor

创建一个只有单个线程对象的线程池,如果需要按顺序执行一系列的任务,就可以使用这种形式,比如按顺序读写文件、再处理文件。

public class SingleThreadExecutorExample {  
    public static void main(String[] args) {  
         ExecutorService executor = Executors.newSingleThreadExecutor();

        // 提交任务到线程池  
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("Task " + taskId + " is running by thread " + Thread.currentThread().getName());
                    // 模拟任务执行时间  
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        // 关闭线程池  
        executor.shutdown();
        while (!executor.isTerminated()) {
            // 等待所有任务完成  
        }

        System.out.println("All tasks are completed.");
    }  
}

newCachedThreadPool

只要有任务执行,但线程池中没有空闲线程,就会新建一个线程来执行任务,适合需要大量短生命周期任务同时运行的场景。

public class CachedThreadPoolExample {  
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();

        // 提交任务到线程池
        for (int i = 0; i < 100; i++) {
            final int taskId = i;
            executor.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("Task " + taskId + " is running by thread " + Thread.currentThread().getName());
                    // 模拟短生命周期任务
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        // 关闭线程池(这里使用优雅关闭)
        executor.shutdown();
        try {
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("All tasks are completed.");
    }

Lambda表达式

被@FunctionalInterface注解标记过的功能接口,可以使用Lambda表达式的写法。功能接口:只有一个方法的接口。

在线程中的写法

  public static void main(String[] args) {


        Runnable runnable1 = new Runnable() {
            @Override
            public void run() {
                System.out.println("helloworld1");
                System.out.println("helloworld2");
                System.out.println("helloworld3");
            }
        };
        new Thread(runnable1).start();


        Runnable runnable2 = () -> {
            System.out.println("helloworld1");
            System.out.println("helloworld2");
            System.out.println("helloworld3");
        };
        new Thread(runnable2).start();

        Runnable runnable3 = () -> System.out.println("helloworld1");
        new Thread(runnable3).start();

        new Thread(() -> System.out.println("helloworld1")).start();


        Thread thread = new Thread(() -> {
            System.out.println("helloworld1");
        });
        thread.start();


        Callable<String> callable = new Callable<>() {
            @Override
            public String call() throws Exception {
                int i = 1 + 2;
                return "计算结果:" + i;
            }
        };


        Callable<String> callable1 = () -> {
            int i = 1 + 2;
            return "计算结果:" + i;
        };
        FutureTask<String> objectFutureTask = new FutureTask<>(callable1);
        FutureTask<String> futureTask = objectFutureTask;
        new Thread(futureTask);

    }

自定义Lambda

//不带参数、不带返回值
@FunctionalInterface
public interface Greeter {

    void greet();

}


Greeter greeter = () -> System.out.println("hello");

Greeter greeter = () -> {
	System.out.println("hello");
	System.out.println("hello");
	};
greeter.greet();


//带一个参数
@FunctionalInterface
public interface Greeter1 {

    void greet(String name);

}

Greeter1 greeter1 = name -> System.out.println("hello");
Greeter1 greeter1 = (String name) -> System.out.println("hello");

//带参数和返回值
@FunctionalInterface
public interface Greeter2 {

    int greet(String name, int i);

}

Greeter2 greeter2 = (String name, int i) -> {
	System.out.println("hello world");
	return 0;
};

Greeter2 greeter2 = (name, i) -> {
	System.out.println("hello world");
	return 0;
    };
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐