配置环境变量:
下载jdk,保存jdk的路径,在计算机属性中的环境变量中新建变量JAVA_HOME为jdk的路径,
在系统变量中的path前加入%JAVA_HOME%\bin;即可
eclipse用法:
先新建一个project,在project中的src中新建一个包package,在包中建一个class(类名最好为Main),package可以导入包
输出hello world:
package java1;//先引入装的文件包
public class HelloWorld {
//类名和文件名称不相同时不用加public
public static void main(String[] args) {
System.out.println(666);//输出内容
}
}
//java语言严格规定大小写
注释:
单行注释://
多行注释:/* */
文档注释:/** */
类:
用class定义类,在一个java源文件中可以声明多个class。但是只能最多有一个类声明为public的。]
而且要求声明为public的类的类名必须与源文件名相同
输出语句:
System.out.println():先输出数据,然后换行
System.out.print():输出数据,不换行
System.out.println():代表换行
转义字符:
在输出当中\n代表换行
\\n表示不转义,输出\n
想要输出“”必须在引号前加\
java定义变量的格式:数据类型 变量名=变量值;
变量符号:
定义整型变量:int、byte、short、long
定义浮点型:float、double
定义字符型:char
定义布尔型:boolean(只能赋值为true或者false)
定义类:class
定义接口:interface
定义数组:[]
定义long类型变量,必须以"l"或"L"结尾
定义float类型变量时,变量要以"f"或"F"结尾
循环:for循环,while循环
for(int i=0;i<n;i++){
循环内容;
}
whie(循环条件){
循环内容;
}
自动类型提升:
结论:当容量小的数据类型的变量与容量大的数据类型的变量做运算时,结果自动提升为容量大的数据类型
数组[]:(花括号可以放在变量之后)
int[] arr4={1,2,3,4,5};
int[] 数组名;//声明数组
静态初始化:数组的初始化和数组元素的赋值同时进行
例:int[] ids;
ids = new int[]{1001,1002,1003,1004}
动态初始化:数组的初始化和数组元素的赋值分开操作
String[] names = new String[5];
int[] arr = new int[4];
names[0]='0'
...
names[4]='5'
总结:数组一旦初始化完成,其长度就确定了
获取数组的长度:数组名.length
整型和浮点型数组元素的默认初始化值为0或0.0
char型数组元素的默认值为空格
String型数组元素的默认值为null
接收键盘输入的数据:
import java.util.Scanner;//先导入包
Scanner scanner = new Scanner(System.in);
int number = scanner.nextInt();
二维数组的声明和初始化:
静态初始化:
int[][] arr1=new int[][]{{1,2,3},{4,5},{6,7,8}};
动态初始化:
String[][] arr2=new String[3][2];
二维数组其实也是一维数组,只不过每个元素都是一个新的数组
二维数组的长度由行数决定
遍历二维数组:
例:int[][] arr1=new int[][]{{1,2,3},{4,5},{6,7,8}};
for(int i=0;i<arr1.length;i++){
for(int j=0;j<arr1[i].length;i++){
System.out.println(arr1[i][j]);
}
}
规定:二维数组分为外层数组的元素,内层数组的元素
外层元素:arr[0],arr[1]等
内层元素:arr[0][0],arr[1][2]等
外层元素的初始化值为:地址值
内层元素的初始化值为:与一维数组一样
生成随机数:
double d=Math.random();//Math.random()生成的随机数范围为[0.0-1.0]
生成n-m之间的随机整数
int num=(int)(Math.random()*(m-n+1)+m)
定义两个数组,把其中一个数组赋值给另一个数组,相当于把一个数组的地址赋值给另一个数组。
想要复制一个数组则需要定义两个长度一样的数组,通过遍历数组把第一个数组的每一个元素给另一个数组
java.util.Arrays类即为操作数组的工具类,包含了用来操作数组的各种方法:
判断两个数组是否相等:boolean equals(int[] a,int[] b)
输出数组信息:String to String(int[] a)
将指定值填充到数组当中:void fill(int[] a,int val)
对数组进行排序:void sort(int[] a)
对排序后的数组进行二分法检索指定的值:int binarySearch(int[] a,int key)
java类以及类的成员:属性、方法、构造器;代码块、内部类;
面向对象的三大特征:封装性、继承性、多态性(抽象性);
public class PersonTest{
public satatic void main(String[] args){
//创建Person类的对象
Person p1=new Person();//开辟一个新的空间创建一个对象
//调用对象的属性和方法:对象.属性
p1.name='Tom';
System.out.println(p1.name);
p1.eat();
p1.sleep();
p1.talk("Chinese");
//对象赋值是给地址
}
}
class Person{
//属性
String name;
int age=1;
bollean isMale;
//方法:
puiblic void eat(){
System.out.println("人可以吃饭");
}
puiblic void sleep(){
System.out.println("人可以睡觉");
}
puiblic void talk(String language){
System.out.println("人可以说话,使用的是:"+language);
}
}
方法的声明:权限修饰符 返回值类型 方法名(形参列表){
方法体
}
如果方法有返回值,则必须在方法声明时写上返回值类型,并且使用return来返回数据
如果方法没有返回值,则方法声明时使用void来表示。通常,没有返回值的方法不加return,加return表示结束方法
例:计算圆的面积
package com.atguigu.contact;
public class HelloWorld {
public static void main(String[] args) {
//创建一个circle实例对象
Circle c1= new Circle();
c1.radius=2.1;
double area=c1.findArea();
System.out.println(area);
}
}
//声明一个Circle对象
class Circle{
double radius;
public double findArea() {
double area =3.14*radius*radius;
return area;
}
}
例:打印十行八列的矩阵
//声明一个Ecert3Test对象
public class Ecert3Test{
public static void main(Sting[] args){
//创建一个Ecert3Test对象
Exert3Test test = new Exert3test();
test.method();
}
public void method(){
for(int i=0;i<10;i++){
for(int j=0;j<8;j++){
System.out.println("* ");
}
System.out.println();
}
}
}
创建一个对象数组(数组的元素可以是任何类型的元素):
例:
public class StudentTest{
public static void main(String[] args){
Student[] stus = new Student[20];//创建一个长度为20的Student类型的数组
for(int i=0;i<stus.length;i++){
//给数组的元素赋值
stus[i] = new Student();
//给Student对象的属性赋值
stus[i].number=(i+1);
//年级[1,6]
stus[i].state=(int)(Math.random()*(6-1+1)+1);
//成绩
stus[i].score =(int)(Math.random()*(100-0+1)+0);
}
//遍历数组
for(int i=0;i<stus.length;i++){
System.out.println(stus[i].info());
//返回结果为一个字符串可以直接输出内容
}
}
}
class Student{
int number;//学号
int state;//年级
int score;//成绩
//显示学生信息的方法
public String info(){
return "学号:"+number+",年级:"+state+",成绩:"+score;
}
}
匿名对象:
创建的对象没有赋值给一个变量名
匿名对象只能调用一次
public class StudentTest{
public static void main(String[] args){
new Student().call();//创建匿名对象并且调用属性和方法
//每new一次就创建一个新的对象
}
}
class Student{
int number;//学号
int state;//年级
int score;//成绩
//显示学生信息的方法
public String info(){
return "学号:"+number+",年级:"+state+",成绩:"+score;
}
public void call(){
System.out.println("打电话");
}
}
匿名对象的使用实例:
public class ceshi{
public static void main(String[] args){
PhoneMall mall = new Phone();
mall.show(new Phone());
//匿名对象适合一次性使用
}
}
class PhoneMall{
public void show(Phone phone){
phone.sendEmail();
phone.playGame();
}
}
class Phone{
public void sendEmail(){
System.out.println("发送邮件");
}
public void playGame(){
System.out.println("打游戏");
}
}
重载的概念:
在同一个类中,允许存在一个以上的同名方法,只要它们的参数个数或者参数类型不同即可
重载的特点:
与返回值类型无关,只看参数列表,且参数列表必须不同。(参数个数或参数类型)。调用时,根据方法参数列表的不同来区别。
重载实例:
//返回两个整数的和
int add(int x,int y){
return x+y;
}
//返回三个整数的和
int add(int x,int x,int z){
return x+y;
}
可变个数形参的方法(不确定参数的个数,可以是0):
可变个数形参的格式:数据类型 ... 变量名
如果在传入可变个数形参的同时还有其他参数,可变个数形参必须放在参数列表的最后
例:
public class ceshi{
public static void main(String[] args){
ceshi test = new ceshi();
test.show("hello","world");
}
public void show(String ... strs){
System.out.println("666");
}
}
如果变量是基本数据类型,此时赋值的是变量所保存的数据值
如果变量是引用数据类型,此时赋值的是变量所保存的数据的地址值
char类型的数组名代表内容,其他数组名代表地址(string类型也算数组)
如果形式参数是一个类,在该函数的作用域内可以使用这个类的全部属性
System.exit(0)可以直接结束程序的运行
在一个包下的不同class文件可以互相调用class文件里面的方法
递归方法:一个方法体内调用它自身
方法递归包含了一种隐式的循环,它会重复执行某段代码,但这种重复执行无须循环控制
递归一定要用return设置出口,否则会成为死循环
面向对象的特征:封装和隐藏
在定义变量的最前方加private,可以把属性设置成为私有的属性,不能直接通过对象.属性进行调用该属性,只能通过方法设置和获取私有化属性
java规定的4种权限(从小到大排列):private、缺省、protected、public
private只有在类的内部才能调用,缺省在类内部、同一个包下才能调用,protected在类内部、同一个包、不同包的子类下才能被调用,public在类内部、同一个包、不同包的子类、同一个工程下才能被调用
例题:创建程序,在其中定义两个文件类,Person和PersonTest类。定义如下:用setAge()设置人的的合法年龄(0-130),getAge()返回人的年龄。
public class Person{
privarte int age;
public void setAge(int a){
if(a<0||a>130){
System.out.println("传入的数据非法");
return;
}
age=a;
}
public int getAge(){
return age;
}
}
public calss PersonTest{
public static void main(String[] args){
Person p1 = new Person();
//p1.age=1;因为私有化,编译不通过
p1.setAge(12);
System.out.println("年龄为:"+p1.getAge());
}
}
构造器(或构造方法、constructor)的使用:
构造器的使用方法:public +类名(参数){},在创建对象时,会执行空参构造器当中的内容
构造器的作用:创建对象、创建对象的同时可以初始化属性
说明:1.如果没有显示的定义类的构造器的话,则系统默认提供一个空参的构造器
2.定义构造器的格式:权限修饰符 类名(形参列表)
3.一个类中可以创建多个构造器,构成重载
4.一旦我们显示的定义了类的构造器之后,系统就不再提供默认的空参构造器
5.一个类中至少会有一个构造器
例:public class PersonTest{
public static void main(String[] args){
//创建类的对象:new +构造器
Person p = new Person("Tom");
p.eat();
}
}
class Person{
String name;
int age;
//构造器
public Person(){
System.out.println("Person()...")
}
public Person(String n){
//创建对象的同时可以传入一个参数
name=n;
}
/*
初始化一个属性
public Person(){
age=19;
}
*/
public void eat(){
System.out.println("吃饭");
}
public void study(){
System.out.println("学习");
}
}
JavaBean:
JavaBean是一种java语言写成的课重用组件
javabean是指符合如下标准的java类:
1.类是公共的
2.有一个无参数的公共构造器
3.有属性、且有对应的get、set方法
this的使用:(this可以代表当前的对象)
在类的方法中,我们可以使用this.属性或者this.方法的方式调用当前对象的属性或者方法
但是,通常情况下可以省略this,如果方法的形参和类的属性同名时,我们必须显示的使用
this.变量的方式表明此变量是属性而不是形参(在构造器中也可以使用this)
this可以调用构造器:
1.我们在类的构造器中,可以显示的使用this(形参列表)方式,调用本类中指定的其他构造器
2.构造器中不能通过this(形参列表)方式调用自己,否则会成为死循环报错
3.this(形参列表)必须声明在当前构造器的首行
例:
class Person(){
private String name;
private int age;
public Person(){
System.out.println("我是一个空参的构造器");
}
public void setName(String name){
this.name=name;
//this代表当前对象,this.name代表当前对象的属性
//name代表形式参数
}
public String getName(){
return name;
}
public void setAge(int age){
this();
//this()可以调用空参的构造器,如果加上不同的参数类型可以调用各种对应参数类型的构造器
this.age=age;
}
public int getAge(){
return age;
}
}
package关键字的作用:
1.为了更好的实现项目中类的管理,提供包的概念
2.使用package声明类或接口所属的包,声明在源文件的首行
3.包,属于标识符,遵循标识符的命名规则
4.每“.”一次,就代表一层文件目录
jdk中主要的包介绍:
java.lang:包含一些java语言的核心类,如String、Math、Integer、System和Thread,提供常用功能
java.net:包含执行与网络相关的操作的类和接口
java.io:包含能提供多种输入/输出的功能的类
java.util:包含一些实用工具类,如定义系统特性、接口的集合框架类、实用与日期日历相关的函数
java.text:包含了一些java格式化相关的类
java.sql:包含了java进行jdbc数据库编程的相关类/接口
java.wat:包含了构成抽象窗口工具集的多个类,这些类被用来构建和管理应用程序的图形用户界面
import关键字的使用:
import导入:
1.在源文件中显示的使用import结构导入指定包下的类、接口
2.声明在包的声明和类的声明之间
3.如果使用的类或接口是java.lang包下定义的,则可以省略import结构
4.如果使用的类或接口是本包下定义的,则可以省略import结构
5.如果在源文件中,使用了不同包下的同名的类,则必须至少有一个类需要以全类名的方式显示,(包名.类名 变量 = new 包名.类名())
6.使用“xxx.*”的方式表明可以调用xxx包下的所有结构,但是如果使用的是xxx子包下的结构,则仍需要显示
7.import static:导入指定类或接口中的静态结构
eclipse快捷键:alt+/
快速修复:ctrl+1
批量自动导入包:ctrl+shift+o
使用单行注释:ctrl+/
使用多行注释:ctrl+shift+/
取消多行注释:ctrl+shift+\
选中的结构的大小写的切换:变成大写:ctrl+shift+x
选中的结构的大小写的切换:变成小写:ctrl+shift+y
在文档中找某个单词的位置:ctrl+k或者ctrl+f
继承的格式:class A extends B{}
A:子类、派生类、subclass
B:父类、超类、基类、superclass
体现:一旦子类A继承父类B以后,子类A中就有了父类B中声明的结构和方法:属性、方法
私有的属性也能被继承,但是不能直接调用,必须使用get和set方法
子类在继承之后还能定义自己方法和属性,实现功能的扩展
一个类可以被多个子类继承,一个类只能有一个类,每个类可以多重继承,爷爷爸爸儿子
如果我们没有显示的声明一个类的父类的话,则此类继承于java.lang.Object类
在eclipse中的source里面的Generate Getters and Setters可以给private的属性书写代码
在eclipse中的source里面的Generate Constructor using Fields可以给构造器书写代码(有空参和全参两种)
重写:子类继承父类以后,可以对父类中同名参数的方法,进行覆盖操作
重写的规定:
方法的声明:权限修饰符 返回值类型 方法名(形参列表){
//方法体
}
1.子类重写的方法和方法名和形参列表与父类中被重写的方法名和形参列表相同
2.子类重写的方法的权限修饰符不小于父类被重写的方法的权限修饰符
>特殊情况:子类不能重写父类中声明为private权限的方法
3返回值类型:
>父类被重写的方法的返回值类型是void,则子类重写的方法的返回值类型只能是void
>父类被重写的方法的返回值是A类型,则子类重写的方法的返回值类型可以是A类或A类的子类
>父类被重写的方法的返回值是基本数据类型,则子类重写的方法的返回值类型必须是相同的基本数据类型
4 子类重写的方法抛出的异常类型不大于父类被重写的方法抛出的异常类型
子类和父类中的同名同参数的方法要么都声明为非static的,要么都声明为static的
super关键字:super可以调用父类的属性、方法
super关键字的使用:如果父类和子类有一个命名相同的属性或者方法,子类在使用时默认是使用自己的属性,只有用super才能使用父类的
super调用构造器:super()表示调用父类中声明的指定的构造器
super(形参列表)的使用,必须声明在子类构造器的首行!
我们再类的构造器中,针对this(形参列表)或super(形参列表)只能二选一,不能同时出现
在构造器的首行,没有显示的声明this(形参列表)或者super(形参列表)会默认调用父类中的空参构造器
子类对象的实例化过程:
1.从结果上看:(继承性)
子类继承父类以后,就获取了父类中声明的属性和方法
创建子类的对象,在堆空间中,就会加载所有父类中声明的属性
2.从过程上看:
当我们通过子类的构造器创建子类的对象时,我们一定会直接或间接的调用其父类的构造器,一直往上,直到调用object类。正因为加载过所有父类的结构,所以才可以看见内存中有父类中的结构,子类对象才可以考虑进行调用
多态性:可以理解为一个事物的多种形态
对象的多态性:父类的引用指向子类的对象(用父类创建一个子类对象)
例:Person为父类,Man为子类
Person p1 = new Man();
多态的使用(多态就是向上转型的方法):
当调用子父类同名同参数的方法时,实际执行的子类重写父类的方法,当调用子父类同名的属性时,用的是父类的属性,实例对象可以使用父类中的方法
p1只能用Person类中定义过的方法,不能调用Man类中Person类中没有的方法
多态性的使用前提:1.类的继承关系 2.方法的重写
对象的多态,只适用于方法,不适用于属性,可以把实例对象当做方法的参数传入到方法当中进行操作
例:
package com.atguigu.contact;
public class Test {
public static void main(String[] args) {
Test test = new Test();
test.func(new Dog());
}
public void func(Animal animal) {//Animal animal = new Dog()
animal.eat();
animal.shout();
}
}
class Animal{
public void eat() {
System.out.println("动物:进食");
}
public void shout() {
System.out.println("动物:叫");
}
}
class Dog extends Animal{
public void eat() {
System.out.println("狗吃骨头");
}
public void shout() {
System.out.println("汪汪汪");
}
}
class Cat extends Animal{
public void eat() {
System.out.println("猫吃鱼");
}
public void shout() {
System.out.println("喵喵喵");
}
}
//如果没有多态性,只能声明什么对象,创建什么对象的类型
//有多态就能只创建一个父类对象,把子类当成参数传入
重载:方法名相同但是形式参数列表不同
重写:子类继承父类之后,自己再创建一个和父类名字相同的方法(重写表现为多态性)
有了对象的多态性以后,内存中实际上是加载了子类特有的属性和方法的,但是由于变量声明为父类类型,导致编译时,只能调用父类中声明的属性和方法。子类特有的属性和方法不能调用。
使用强制类型转换符才能调用子类特有的属性和方法,把父类转为子类(向下转型),使用强转时,可能出现异常
Animal a = new Dog();
Dog dog = (DOg)a;
此时dog就可以使用自身包含的方法
instanceof关键字的使用:
a instanceof A:判断对象a是否是类A的实例,返回布尔类型变量(向下转型时需要使用instanceof判断)
equals可以比较两个对象地址是否相同,可进行覆盖,比较两个对象的内容是否相同
==和equals()的区别:
==:运算符
1.可以使用在基本数据类型变量和引用数据类型变量中
2.如果比较的是基本数据类型变量,比较两个变量保存的数据是否相等(不一定要类型相同)
如果比较的是引用数据类型变量,比较两个对象的地址值是否相同
equals()方法的使用:
1.是一个方法,而非运算符
object类中定义的equals()和==的作用是相同的,比较两个对象的地址值是否相同
像String、Date、File、包装类等都重写了object类中的equals()方法。重写以后比较的是两个对象的实体内容是否相同
重写equals:
public boolean equals(Object obj){
//1.判断两个对象是否是同一个引用
if(this==obj){
return true;
}
//2.判断obj是否null
if(obj==null){
return false;
}
//3.判断是否是同一个类型
if(this.getClass()==obj.getClass()){
}
//instanceof判断对象是否是某种类型
}
static关键字:
在java中使用static关键字来修饰成员变量,该变量被称作静态变量,它可以被所有实例所共享
静态变量可以使用如下语法来访问:类名.变量名
例:统计学生的人数(创建一个学生对象就让静态属性count+1,则可以把count+1写在学生类的无参构造器当中,就可以做到创建一个对象增加一个数据)
例:
class Student{
static String schoolName;
}
public class Example12{
public static void main(String[] args){
Student stu1 = new Student();
Student stu2 = new Student();
Student.schoolNmae="清华大学";
System.out.println("我是"+stu1.schoolName+"的学生");
System.out.println("我是"+stu2.schoolName+"的学生");
}
}
//由于schoolName是静态变量,因此可以直接使用Student.schoolName的方式进行调用,也可以通过Student的实例对象进行调用。
静态方法:
开发人员有时会希望在不创建对象的情况下就可以调用某个方法,这种情况就可以使用静态方法,静态方法的定义十分简单,只需要在类中定义的方法前加上static关键字即可.
静态方法允许直接访问静态成员,静态方法不能直接访问非静态成员
静态方法中不允许使用this或者是super关键字,静态方法可以继承,不能重写、没有多态
静态方法可以通过如下两种方式来访问:
类名.方法 或 实例对象名.方法
例:
class Person{
public static void say(){
System.out.println("Hello");
}
}
public class Example13{
public static void main(String[] args){
//用类名.方法的方式调用静态方法
Person.say();
//实例化对象
Person person = new Person();
//用实例对象名.方法的方式来调用静态方法
person.say();
}
}
静态代码块:
在java中使用一对大括号包围起来的若干行代码被称为一个代码块,用static关键字修饰的代码块称为静态代码块
静态代码块的语法如下:
static{
...
}
当类被加载时,静态代码块会执行,由于类只加载一次,因此静态代码块也只执行一次,在程序中,通常会使用静态代码块来对类的成员变量进行初始化。
例:
class Person{
static{
System.out.println("执行了person类中的静态代码块");
}
}
public class Example14{
static{
System.out.println("执行了测试类中的静态代码块");
}
public static void main(String[] args){
Person p1 = new Person();
Person p2 = new Person();
}
}
输出结果:
执行了测试类中的静态代码块
执行了Person类中的静态代码块
java虚拟机会先加载类Example14,在加载类的同时就会执行该类的静态代码块,紧接着就会调用main()方法,在main()方法中创建了两个Person对象,
但是在两次实例化对象的过程中,静态代码块的内容只输出了一次,这就说明静态代码块在类第一次使用时才会被加载,并且只会加载一次
final关键字:
final关键字可以用于修饰类、变量和方法,被final修饰的类、变量和方法将具有以下特性:
1.final修饰的类不能被继承
2.final修饰的方法不能被子类重写
3.final修饰的变量(成员变量和局部变量)是常量,只能赋值一次
抽象类(不能创建实例对象):
当定义一个类时,常常需要定义一些方法来描述该类的行为特征,但有时这些方法的实现方式是无法确定的。
抽象方法必须使用abstract关键字来修饰,并且在定义方法时不需要实现方法体。当一个类中包含了抽象方法,那么该类也必须使用abstract关键字来修饰,这种使用abstract关键字修饰的类就是抽象类。
抽象类及抽象方法定义的基本语法:
//定义抽象类
[修饰符] abstract 类名{
//定义抽象方法
[修饰符] abstract 方法返回值类型 方法名(参数列表);
}
包含抽象方法的类必须定义为抽象类,但抽象类中可以不包含任何抽象方法。
抽象类是不可以实例化的,因为抽象类中有可能包含抽象方法,抽象方法是没有方法体的,不可以被调用。
抽象类中不一定有抽象方法,但有抽象方法的类一定是抽象类.
例:
sbstract class Animal{
//定义抽象方法shout()
public abstract void shout()
}
//定义Dog类继承Animal
class Dog extends Animal{
//实现抽象方法shout(),编写方法体
public void shout(){
System.out.println("汪汪......");
}
}
//定义测试类
public class Example12{
public static void main(String[] args){
Dog dog = new Dog();//创建Dog类的实例对象
dog.shout();//调用dog对象的shout()方法
}
}
接口:
如果一个抽象类中的所有方法都是抽象的,则可以将这个类定义为java中的另一种形式--接口。接口是一种特殊的抽象类,它不能包含普通方法,其内部的所有方法都是抽象方法,它将抽象进行得更为彻底.
接口中除了抽象方法以外,还可以有默认方法和静态方法,默认方法使用default修饰,静态方法使用static修饰,并且这两种方法都允许有方法体,在定义接口时,使用interface关键字来声明。
接口没有构造方法,不能创建对象,接口可以用类来实现,一个类可以实现多个接口
接口也可以通过多态来创建实例化对象
接口定义的基本语法:
[修饰符] interface 接口名 [extends 父接口1,父接口2,...]{
[public] [static] [final] 常量类型 常量名 = 常量值;
[public] [abstract] 方法返回值类型 方法名([参数列表]);
}
[public] default 方法返回值类型 方法名([参数列表]){
//默认方法的方法体
}
[public] static 方法返回值类型 方法名([参数列表]){
//静态方法的方法体
}
例1:
//定义了Animal接口
interface Animal{
int ID = 1;
void breathe();
//定义一个默认方法
default void getType(String type){
System.out.println("该动物属于:"+type);
}
//定义一个静态方法
static int getID(){
return Animal.ID;
}
}
//Dog类实现了Animal接口
class Dog implements Animal{
//实现了breathe()方法
public void breathe(){
System.out.println("狗在呼吸");
}
}
//定义测试类
public class Example13{
public static void main(String[] args){
System.out.println(Animal.getID()); //通过接口名调用类方法
Dog dog = new Dog(); //创建Dog类的实例对象
System.out.println(dog.ID); //在实现类中获取接口全局常量
dog.breathe(); //调用dog对象的breathe()方法
dog.getType("犬科"); //通过接口实现类
}
}
输出结果:
1
1
狗在呼吸
该动物属于:犬科
Dog类通过implements关键字实现了Animal接口,并实现了接口中的抽象方法breathe().
例2:
//定义了Animal接口
interface Animal{
int ID=1;
void breathe();
//定义了一个默认方法
default void getType(String type){
System.out.println("该动物属于:"+type);
}
//定义一个静态方法
static int getID{
return Animal.ID;
}
}
//定义了LandAnimal接口,并继承Animal接口
interface LandAnimal extends Animal{
void run();
}
//Dog类实现了LandAnimal接口
class Dog implements LandAnimal{
//实现breathe()方法
public void breathe(){
System.out.println("狗在呼吸");
}
//实现run()方法
public void run(){
System.out.println("狗在陆地上跑");
}
}
//定义测试类
public class Example14{
public static void main(String[] args){
System.out.println(Animal.getID);
Dog dog = new Dog();
System.out.println("dog.ID");
dog.breath();
dog.getType("犬科");
dog.run();
}
}
例4-14中,定义了两个接口,其中LandAnimal接口继承了Animal接口,因此LandAnimal接口中包含了两个抽象方法.
内部类:
在java中,允许在一个类的内部定义类,这样的类称为内部类,这个内部类所在的类称为外部类。
例:
//定义外部类Outer
class Outer{
int m=0;
//定义外部类成员方法
void test1(){
System.out.println("外部类成员方法");
}
//定义成员内部类
class Inner{
int n=1;
//1.定义内部类方法,访问外部类成员变量和方法
void show1(){
System.out.println("外部类成员变量m="+m);
test1();
}
void show2(){
System.out.println("内部类成员方法");
}
}
//定义外部类方法,访问内部类变量和方法
void test2(){
Inner inner = new Inner();
System.out.println("内部类成员变量n="+inner.n);
inner.show2();
}
}
//定义测试类
public class Example18{
public static void main(String[] args){
Outer outer = new Outer();//创建外部类对象
Outer.inner inner = outer.new Inner();//创建内部类对象
inner.show1();//测试在成员内部类中访问外部类成员变量和方法
outer.test2();//测试在外部类中访问内部类成员变量和方法
}
}
运行结果:
外部类成员变量m=0
外部类成员方法
内部类成员变量n=1
内部类成员方法
成员内部类可以访问外部类所有成员,同时外部类也可以访问成员内部类的所有成员。
创建内部类对象的具体格式:
外部类名.内部类名 变量名 = new 外部类().new 内部类();
如果内部类和外部类有相同的属性,内部类输出该属性默认为自己内部的属性,需要访问到外部类的属性需要 外部类名.this.属性名
局部内部类:
局部内部类,也叫做方法内部类,就是定义在某个局部范围中的类,它和局部变量一样,都是在方法中定义的,其有效范围只限于方法内部.
在局部内部类中,局部内部类可以访问外部类的所有成员变量和方法,而局部内部类中的变量和方法却只能在创建该局部内部类的方法进行访问。
局部内部类可以直接访问内部类的属性,局部内部类访问外部类的属性需要使用 外部类名.this.属性名
例:
//定义外部类Outer
class Outer{
int m=0;
void test1{
System.out.println("外部类成员方法");
}
void test2(){
//1.定义局部内部类Inner,在局部内部类中访问外部类变量和方法
class Inner{
int n=1;
void show(){
System.out.println("外部类变量m="+m);
}
}
//2.创建局部内部类的方法中,调用局部内部类变量和方法
Inner inner = new Inner();
System.out.println("局部内部变量n="+inner.n);
inner.show();
}
}
//定义测试类
public class Example19{
public static void main(String[] args){
Outer outer = new Outer();
outer.test2(); //通过外部类对象调用创建了局部内部类的方法
}
}
运行结果:
局部内部类变量n=1
外部类变量m=0
外部类成员方法
局部内部类可以访问外部类所有成员,而只有在包含局部内部类的方法中才可以访问内部类的所有成员
静态内部类:
静态内部类只是在内部类前增加了static关键字,但在功能上,静态内部类只能访问外部类的静态成员,同时通过外部类访问静态内部类成员时,可以跳过外部类从而直接通过内部类访问静态内部类成员.静态内部类访问外部类的普通属性时需要创建一个外部类的实例对象
创建静态内部类对象的基本语法:
外部类名.静态内部类名 变量名 = new 外部类名.静态内部类名();
例:
package test1;
//定义测试类
public class Test {
public static void main(String[] args) {
//静态内部类可以直接通过外部类创建
Outer.Inner inner = new Outer.Inner();
inner.show();
}
}
class Outer{
static int m=0;//定义外部类静态变量m
static class Inner {
void show() {
//静态内部类访问外部类静态成员
System.out.println("外部类静态变量m=" + m);
}
}
}
运行结果:
外部类静态变量m=0
匿名内部类:
在java中调用某个方法时,如果该方法的参数是一个接口类型,除了可以传入一个参数接口实现类,还可以使用匿名内部类实现接口来作为该方法的参数。
匿名内部类其实就是没有名称的内部类,在调用包含有接口类型参数的方法时,通常为了简化代码,不会创建一个接口的实现类作为方法传入,而是直接通过匿名内部类的形式传入一个接口类型参数,在匿名内部类中直接完成方法的实现。
创建匿名内部类的基本语法格式如下:
new 父接口(){
//匿名内部类实现部分
}
例:
package test1;
//定义测试类
public class Test {
public static void main(String[] args) {
String name="小花";
//定义匿名内部类作为参数传递给animalShout()方法
animalShout(new Animal() {
//实现shout()方法
public void shout() {
//JDk8开始,局部内部类、匿名内部类可以访问非final的局部变量
System.out.println(name+"喵喵...");
}
});
}
//定义静态方法animalShout(),接收接口类型参数
public static void animalShout(Animal an){
an.shout();//调用传入对象an的shout()方法
}
}
//定义动物类接口
interface Animal{
void shout();
}
运行结果:
小花喵喵...
Lambda表达式入门:
匿名内部类存在的一个问题是,如果匿名内部类的实现非常简单,例如只包含一个抽象方法的接口,那么匿名内部类的语法仍然显得比较冗余。
一个Lambda表达式由三个部分组成,分别为参数列表、"->"和表达式主体,其语法格式为:([数据类型 参数名,参数类型 参数名,...]->{表达式主体})
(1):([数据类型 参数名,参数类型 参数名,...]:用来向表达式主体传递接口方法需要的参数,多个参数名中间必须用英文逗号进行分隔。
(2):->:表示Lambda表达式箭牌,用来指定参数数据指向,不能省略,并且用英文横线和大于号书写.
(3):{表达式主体}:由单个表达式或语句块组成的主体,本质就是接口中抽象方法的具体实现,如果表达式主体只有一条语句,那么可以省略包含主体的大括号。
例:
package test1;
//定义测试类
public class Test {
public static void main(String[] args) {
String name = "小花";
//1.匿名内部类作为参数传递给animalShout()方法
animalShout(new Animal(){
public void shout () {
System.out.println("匿名内部类输出:" + name + "喵喵...");
}
});
//2.使用Lambda表达式作为参数传递给animalShout()方法
animalShout(() -> System.out.println("Lambda表达式输出:" + name + "喵喵..."));
}
//创建一个animalShout()静态方法,接收接口类型的参数
public static void animalShout (Animal an){
an.shout();
}
}
//定义动物类接口
interface Animal{
void shout();
}
运行结果:
匿名内部类输出:小花喵喵...
Lambda表达式输出:小花喵喵...
lambda
函数式接口:
虽然Lambda表达式可以实现匿名内部类的功能,但在使用时却有一个局限,即接口中有且只有一个抽象方法才能使用Lambda表达式代替匿名内部类。这是因为Lambda表达式是基于函数式接口实现的,所谓函数式接口是指有且仅有一个抽象方法的接口,只有确保接口中有且仅有一个抽象方法,Lambda表达式才能顺利地推导出所实现的这个接口中的方法。
java专门为函数式接口引入了一个@FunctionalInterface注解,该注解只是显示地标注了接口是一个函数式接口,并强制编辑器进行更严格的检查,确保该接口是函数式接口,如果不是函数式接口,那么编译器就会报错,而对程序运行并没有实质上的影响。
例:
package test1;
//定义无参,无返回值的函数式接口
@FunctionalInterface
interface Animal{
void shout();
}
//定义有参、有返回值的函数式接口
interface Calculate{
int sum(int a,int b);
}
public class Test{
public static void main(String[] args) {
//分别对两个函数式接口进行测试
animalShout(()->System.out.println("无参、无返回值的函数式接口调用"));
showSum(10,20,(x,y)->x+y);
}
//创建一个动物叫的方法,并传入接口对象Animal作为参数
private static void animalShout(Animal animal){
animal.shout();
}
//创建一个求和的方法,并传入两个int类型以及接口Caculate类型的参数
private static void showSum(int x,int y,Calculate calculate){
System.out.println(x+"+"+y+"的和为"+calculate.sum(x,y));
}
}
先定义了两个函数式接口Animal和Calculate,然后在测试类中分别编写了两个静态方法,并将这两个函数接口以参数的形式传入,最后在main()方法中分别调用这两个静态方法,并将所需要的函数式接口参数以Lambda表达式的形式传入。
接口的多态:
public class TestPolymorpfic{
public static void main(String[] args){
Dog myDog = new Dog();
Animal a = myDog;
Runnable r = myDog;
Swimmable s = myDog;
}
}
interface Runnable{
public abstract void run();
}
interface Swimmable{
public abstract void swim();
}
abstract class Animal{
public void eat(){};//父类方法
public void sleep(){};//父类方法
}
class Dog extends Animal implements Runnable,Swimmable{
public void run(){};//接口方法
public void swim(){};//接口方法
public void shout(){};//独有方法
}
多种不同类型的引用指向同一个对象时,表示看待对象的视角不同。
Dog myDog:将狗当狗看,会全部的功能和方法
Animal a:将狗当动物看,只能执行吃和睡
Runnable r:将狗当会跑的东西看,只能跑
Swimmable s:将狗当会游的东西看,只能游
不同引用所能看到的对象范围不同,只能调用自身类型中所声明的部分.
finalize()方法:
当对象被判定为垃圾对象时,由JVM自动调用此方法,用以标记垃圾对象
垃圾对象:没有有效引用指向此对象时,为垃圾对象
垃圾回收:由GC销毁垃圾对象,释放数据存储空间
自动回收机制:JVM的内存耗尽,一次性回收所有垃圾对象
手动回收机制:使用System.gc();通知JVM执行垃圾回收。
包装类:
包装类对应:
基本数据类型 包装类型
byte Byte
short Short
int Integer
long Long
float Float
double Double
boolean Boolean
char Character
类型转换与装箱、拆箱:
例:
package test1;
public class Test{
public static void main(String[] args) {
int age = 30;
//自动装箱
Integer integer1 = age;
System.out.println("自动装箱");
System.out.println(integer1);
//自动拆箱
int age2=integer1;
System.out.println("自动拆箱");
System.out.println(age2);
}
}
基本类型和字符串之间转换
//1.基本类型转成字符串
int n1=100;
//1.1使用+号
String s1=n1+"";
//1.2使用Integer中的toString()方法
String s2=Integer.toString(n1);
//2.字符串转成基本类型
String str="150";
//使用Integer.parseXXX();
int n2=Integer.parseInt(str);
//boolean字符串形式转成基本类型,"true"--->true 非"true"--->false
异常:
常见异常:
NUllPointerException 空指针异常
ArrayIndexOutOfBoundsException 数组越界异常
ClassCastException 类型转换异常
NumberFormatException 数字格式化异常
ArithmeticException 算术异常
当程序在运行时遇到不符合规范的代码或结果时,会产生异常或程序员使用throw关键字手动抛出异常
异常的传递:按照方法的调用链反向传递,如始终没有处理异常,最终会由JVM进行默认异常处理(打印堆栈跟踪信息)
异常处理:
try:执行可能产生异常的代码
catch(Exception e):捕获异常,并处理
finally:无论是否发生异常,代码总能执行
throw:手动抛出异常
throws:声明方法可能要抛出的各种异常
try {} catch{} catch{}:多重catch,遵循从子(小)到父(大)的顺序,父类异常在最后
在catch中e.printStack();和System.out.println(e.getMessage());都可以打印错误信息
手动退出:System.exit(0);
声明异常:
throws关键字:声明异常
例:
package test1;
import java.util.Scanner;
public class Test{
public static void main(String[] args) {
try {
divide();
} catch (Exception e){
e.printStackTrace();
}
}
public static void divide() throws Exception{
Scanner input = new Scanner(System.in);
System.out.println("请输入第一个数字");
int num1 = input.nextInt();
System.out.println("请输入第二个数字");
int num2 = input.nextInt();
int result = num1/num2;
System.out.println("结果:"+result);
}
}
throw抛出异常:
throw new 异常类型("异常信息")
例:
public void setAge(int age){
if(age>0并且age<=120){
this.age=age;
}else{
throw new RuntimeException("年龄不符合要求");
}
}
自定义异常:
需要继承自Exception或Exception的子类,例如RuntimeException(快捷方式Generate Constructors from Superclass),常用RuntiomeException或者自定义异常
必要提供的构造方法:
无参数构造方法
String message参数的构造方法
线程:
一个程序运行后至少有一个进程,一个进程由一个或多个线程组成,彼此间完成不同的工作,同时执行,称为多线程
进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位
线程的特点:
1.线程抢占式执行
效率高
可防止单一线程长时间独占CPU
2.在单核CPU中,宏观上同时执行,微观上顺序执行
创建线程的三种方式:
1.继承Thread类,重写run方法
2.实现Runnable接口
3.实现Callable接口
第一种方式:继承Thread类
public class TestCreateThread{
public static void main(String[] args){
//3.创建子类对象
MyThread t1 = new MyThread();
t1.start();
//调用start()方法来启动线程,不能调用run()方法
//主线程执行代码
for(int i=0;i<50;i++){
System.out.println("主线程==="+i);
}
}
}
//1.继承Thread类
class MyThread extends Thread{
public void run(){
//2.覆盖run方法
for(int i = i;i <= 50;i++){
System.out.println("Mythread:"+i);
}
}
}
输出的结果为主线程和子线程交替执行输出(每次执行的结果都不一样)
获取线程ID和线程名称:
1.在Thread的子类中调用this.getId()获取线程ID或this.getName()获取线程名称(继承Thread才能使用这两种方法)
2.使用Thread.currentThread().getId()和Thread.currentThread().getName(),Thread.currentThread()代表获取当前线程
修改线程名称:
1.调用线程对象的 对象名.setName("修改的名称")方法可以修改线程的名称
2.使用线程子类的构造方法赋值(创建一个类继承Thread,在类中创建两个构造器,一个为无参的构造器,一个为有参数的构造器,形式参数为name字符串,在构造器中使用super(name))
例:
package test1;
import java.util.Scanner;
public class Test{
public static void main(String[] args) {
//创建4个窗口
TickWin w1=new TickWin("窗口1");
TickWin w2=new TickWin("窗口2");
TickWin w3=new TickWin("窗口3");
TickWin w4=new TickWin("窗口4");
//启动线程
w1.start();
w2.start();
w3.start();
w4.start();
}
}
class TickWin extends Thread{
public TickWin(){
}
public TickWin(String name){
super(name);
}
private int ticket=100;//票
public void run(){
//卖票
while(true){
if(ticket<=0){
break;
}
System.out.println(Thread.currentThread().getName()+"卖了第"+ticket+"票");
ticket--;
}
}
}
第二种方式:实现Runnable接口
package test1;
import java.util.Scanner;
public class Test{
public static void main(String[] args) {
//3.创建实现类对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t2 = new Thread(mr);
//调用start()方法
t2.start();
}
}
//1.实现Runnable接口
class MyRunnable implements Runnable{
//2.覆盖run方法
public void run(){
for (int i=1;i<50;i++){
System.out.println("MyRunnable"+i);
}
}
}
线程休眠:
例:
package test1;
import java.util.Scanner;
import static java.lang.Thread.sleep;
public class Test{
public static void main(String[] args) {
SleepThread s1=new SleepThread();
s1.start();
}
}
class SleepThread extends Thread{
public void run(){
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"..."+i);
try {
Thread.sleep(1000);
//当前线程主动休眠1000毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
线程放弃:
当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片。
例:
package test1;
import java.util.Scanner;
import static java.lang.Thread.sleep;
public class Test{
public static void main(String[] args) {
s1 y1 = new s1();
y1.start();
}
}
class s1 extends Thread{
public void run(){
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"..."+i);
Thread.yield();
}
}
}
线程加入:
允许其他线程加入到当前线程当中
例:
package test1;
import java.util.Scanner;
import static java.lang.Thread.sleep;
public class Test{
public static void main(String[] args) {
s1 y1 = new s1();
y1.start();
try {
y1.join();//加入当前线程,并阻塞当前线程,直到加入线程执行完毕
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"==="+i);
}
}
}
class s1 extends Thread{
public void run(){
for(int i=0;i<50;i++){
System.out.println(Thread.currentThread().getName()+"..."+i);
Thread.yield();
}
}
}
设置线程优先级
优先级:
线程对象.setPriority(优先级等级);
线程优先级为1-10,默认为5,优先级越高,表示获取CPU机会越多
守护线程:
线程对象.setDaemon(true);设置为守护线程
线程有两类:用户线程(前台线程)、守护线程(后台线程)
如果程序中所有前台线程都执行完毕了,后台线程会自动结束(后台线程为守护线程)
垃圾回收器线程属于守护线程
多线程安全问题:
当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致.
临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。
原子操作:不可分割的多步操作,被视为一个整体,其顺序和步骤不可打乱或缺省
线程安全:
同步方式(1):
同步代码块:
synchronized(临界资源对象){//对临界资源对象加锁
//代码(原子操作)
}
注:
每个对象都有一个互斥锁标记,用来分配给线程的。
只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块。
线程对出同步代码块时,会释放相应的互斥锁标记。
例:
package test1;
import java.util.Arrays;
import java.util.Scanner;
import static java.lang.Thread.sleep;
public class Test{
private static int index=0;
public static void main(String[] args) throws InterruptedException {
//创建数组
String[] s=new String[5];
//创建两个操作
Runnable runnableA=new Runnable() {
@Override
public void run() {
//同步代码块
synchronized (s){
s[index]="hello";
index++;
}
}
};
Runnable runnableB=new Runnable() {
@Override
public void run() {
synchronized (s){
s[index]="world";
index++;
}
}
};
//创建两个线程对象
Thread a=new Thread(runnableA,"A");
Thread b=new Thread(runnableB,"B");
a.start();
b.start();
a.join();//加入线程
b.join();//加入线程
System.out.println(Arrays.toString(s));
}
}
使用synchronized可以保证两个数据都能插入到数组中,但是不能保证是哪个元素在前面
2.创建锁
例:
package test1;
import java.util.Arrays;
import java.util.Scanner;
import static java.lang.Thread.sleep;
public class Test{
public static void main(String[] args) throws InterruptedException {
//创建Ticket
Ticket ticket=new Ticket();
//创建线程对象
Thread w1=new Thread(ticket,"窗口1");
Thread w2=new Thread(ticket,"窗口2");
Thread w3=new Thread(ticket,"窗口3");
Thread w4=new Thread(ticket,"窗口4");
//启动线程
w1.start();
w2.start();
w3.start();
w4.start();
}
}
class Ticket implements Runnable{
private int ticket=100;
//创建锁
private Object obj=new Object();
@Override
public void run() {
while (true){
synchronized (obj){
if(ticket<=0) {
break;
}
System.out.println(Thread.currentThread().getName()+"卖了第"+ticket+"票");
ticket--;
}
}
}
}
同步方法(非静态方法的锁是this,静态方法的锁是类名.class):
synchronized 返回值类型 方法名称(形参列表0){//对当前对象(this)加锁
//代码(原子操作)
}
注:只有拥有对象互斥锁标记的线程,才能进入该对象加锁的同步方法中
同步规则:
注意:
只要在调用包含同步代码块的方法,或者同步方法时,才需要对象的锁标记。
如调用不包含同步代码块的方法或普通方法时,则不需要锁标记,可直接调用.
例:
1)、定义一个接口IAssaultable(可攻击的),该接口有一个抽象方法attack()。
(2)、定义一个接口IMobile(可移动的),该接口有一个抽象方法move()。
(3)、定义一个抽象类Weapon,实现IAssaultable接口和IMobile接口,但并没有给出具体的实现方法。
(4)、定义3个类:Tank,Flighter,WarShip都继承自Weapon,分别用不同的方式实现Weapon类中的抽象方法。
(5)、写一个类Army,代表一支军队,这个类有一个属性是Weapon数组w(用来存储该军队所拥有的所有武器);该类还提供一个构造方法,在构造方法里通过传一个int类型的参数来限定该类所能拥有的最大武器数量,并用这一大小来初始化数组w。该类还提供一个方法addWeapon(Weapon wa),表示把参数wa所代表的武器加入到数组w中。在这个类中还定义两个方法attackAll()和moveAll(),让w数组中的所有武器攻击和移动。(提示:数组w中加入武器时,使用static关键字定义的变量,来记录索引位置信息)
(6)、写一个主方法去测试以上程序。(提示:Weapon w = new Tank())
package com.atguigu.contact;
public class Test{
public static void main(String[] args) {
Army army=new Army(1);
Weapon w=new Tank();
army.addWeapon(w);
army.attackAll();
army.moveAll();
}
}
interface IAssaultable{
public abstract void attack();
}
interface IMoible{
public abstract void move();
}
abstract class Weapon implements IAssaultable,IMoible{
}
class Tank extends Weapon{
@Override
public void attack() {
System.out.println("Tank开始攻击");
}
@Override
public void move() {
System.out.println("Tank开始移动");
}
}
class Flighter extends Weapon{
@Override
public void attack() {
System.out.println("Flighter开始攻击");
}
@Override
public void move() {
System.out.println("Flighter开始移动");
}
}
class WarShip extends Weapon{
@Override
public void attack() {
System.out.println("WarShip开始攻击");
}
@Override
public void move() {
System.out.println("WarShip开始移动");
}
}
class Army{
static int index=0;
public Weapon[] w;
public Army(int a){
w = new Weapon[a];
}
public void addWeapon(Weapon wa){
w[index]=wa;
index++;
}
public void attackAll(){
for (int i=0;i<w.length-1;i++){
w[i].attack();
}
}
public void moveAll(){
for (int i=0;i<w.length-1;i++){
w[i].move();
}
}
}
例:把字符串当中的大写转化为小写,小写转化为大写
package test1;
import java.util.*;
public class Test{
public static void main(String[] args){
String s1 = new String("Hello world");
char[] s2 = s1.toCharArray();
for(int i=0;i<s2.length;i++){
if(Character.isUpperCase(s2[i])){
s2[i] = Character.toLowerCase(s2[i]);
}
else if(Character.isLowerCase(s2[i])){
s2[i] = Character.toUpperCase(s2[i]);
}
}
System.out.println(s2);
}
}
死锁:
当第一个线程拥有A对象锁标记,并等待B对象锁标记,同时第一个线程拥有B对象标记,并等待A对象锁标记时,产生死锁。
一个线程可以同时拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁
java笔记
最新推荐文章于 2024-05-08 16:50:15 发布