Java入门到精通


Java平台有三个版本:

  • JavaME: 微型版,用于开发小型设备、智能卡、移动终端应用;(使用率较低)
  • JavaSE:标准版,用于创建桌面应用。(企业用JavaSE创建桌面应用较少)
  • JavaEE:企业版,用于创建企业应用。(JavaEE是JavaSE的升级版,语言基础依然是JavaSE,核心算法依然使用JavaSE)

面向对象思想

面向对象思想(Object Oriented Programming,OOP)是一种软件开发方法,将实体抽象成类,它强调将程序看作是一组对象的集合,每个对象都有自己的状态和行为,并且可以与其他对象进行交互。这种思想的核心是封装、继承和多态。

重写: 也叫覆盖,发生在子类或者接口当中,方法名、参数列表、返回值类型要保持一致,或者返回值是其子类;访问修饰符范围必须要比父类大或者一致,抛出的异常类型也不能比父类更多
重载: 发生在同一类当中,方法名要保持一致,参数列表要不一致(数量、类型、顺序),返回值可以是任何类型,也可以具有不同的访问修饰符,也可以抛出不同的异常类型

方法重载的作用:

  1. 提高代码复用性:在实际开发中,有时候需要实现类似的功能,但是需要接收不同类型或者数量的参数,这时候我们可以使用方法重载,避免在同一类中写多个类似的方法,提高了代码复用性
  2. 方便调用:通过方法重载,我们能够实现用相同的方法名调用不同的方法,避免了程序员花费大量时间去记忆不同的方法名称,提高了编程效率
  3. 使程序更加灵活:当某个方法无法完全满足需求时,我们可以通过重载同名方法来扩展其功能,使程序更加灵活可扩展

构造方法: 用于创建和实例化对象

构造方法具有以下特点

  1. 构造方法与类名相同,没有返回值类型(连void都没有)

  2. 构造方法可以被重载,可以定义多个不同参数列表的构造方法;但注意不能被重写!不能被继承!子类只能通过super()方法来调用

  3. 如果没有定义任何构造方法,编译器会自动生成一个默认构造方法(public 类名(){}),该方法不接受任何参数并且什么也不做

  4. 如果定义了至少一个构造方法,则默认构造方法不再生成,需要显式定义

    this关键字指代本类中的属性或方法,但不能放在静态方法中
    new一个对象后,可以.方法也可以.属性
    
public Account(){
//使用this调用本类重载的其他构造方法
this("000","123456",0.0);
}

实例块和静态块的区别:

public class TestBlock {
 private int x;
 {	//每次调用构造方法前自动调用
  System.out.println("实例块");
 }
 static{	//类加载时被调用一次,仅一次,与是否创建对象无关
  System.out.println("静态块");
 }
 public static void main(String[] args) {
  new TestBlock();
  new TestBlock();
 }
}
//运行结果是:
//静态块
//实例块
//实例块

内部类:
Java内部类是定义在另一个类中的类,它包含在外部类的作用域内。使用内部类可以访问外部类的成员变量和方法,同时也可以被外部类访问。
Java内部类的语法如下:

class OuterClass {
    // 外部类代码
    class InnerClass {
        // 内部类代码
    }
}

要使用内部类,可以利用外部类的实例来创建内部类的对象,例如:

OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.new InnerClass();

创建内部类实例的时候需要使用外部类的实例进行调用,Java内部类的主要作用是实现一些隐藏、私有化、封装化等应用场景。例如,某些类只需要在外部类中被使用,而不需要在整个应用程序中暴露,这时可以将该类定义为内部类。此外,内部类中还可以声明静态成员和方法

标识符的命名约定:(驼峰式命名)

  • 类和接口名:每个单词首字母大写,例如MyClass
  • 方法名:首字母小写,其余单词首字母大写,例如setTime()

问号表达式:
三元运算符(问号运算符)的格式:

//test1是一个布尔表达式,如果值为true,则取test2的值,为false则取test3的值
test1 ? test2 : test3;

权限访问修饰符,从大到小为:

  • public:公共权限,可以被任意类访问
  • protected:受保护权限,可被同包类访问,如不是同包类,必须是该类的子类才可以访问
  • default:同包权限,可被同包类访问
  • private:私有权限,只能在本类中访问

三大特性

封装性:
在Java中,封装的具体实现方式是使用访问修饰符对成员变量和方法进行限制,同时提供公共的方法(Getter/Setter)用于访问和修改成员变量。以下是一个简单的示例:

public class Student {
    private String name;
    private int age;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        if(age >= 0 && age <= 120) {
            this.age = age;
        } else {
            System.out.println("Invalid age value!");
        }
    }
}

在上面的示例中,Student类有两个私有的成员变量name和age,分别表示学生的姓名和年龄,通过提供公共的Getter/Setter方法对外暴露这些成员变量。在Getter/Setter方法中,可以加入一些逻辑判断和约束条件,确保内部数据的安全性。比如上面的示例中,setAge方法中限制了age值必须在0到120之间。这样就实现了对数据的封装,在外部只能通过公共的接口访问和修改内部数据,确保了代码的可维护性和安全性。

继承 是指一个对象可以从另外一个对象继承其属性和方法,注意被final修饰的属性和方法不能被继承或者覆盖。通过继承,可以减少代码的重复性,提高代码的复用性和可维护性

多态 是指同一个方法可以在不同的对象上产生不同的行为。通过多态,可以实现代码的灵活性和可扩展性。多态是面向对象思想的核心之一,它使得程序可以根据不同的情况做出不同的响应

多态的具体体现:

class Animal {
    public void makeSound() {
        System.out.println("动物发出声音");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("狗发出汪汪声");
    }
}

class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("猫发出喵喵声");
    }
}

public class PolymorphismExample {
    public static void main(String[] args) {
        Animal animal1 = new Dog();
        Animal animal2 = new Cat();
        
        animal1.makeSound(); // 调用的是子类Dog的makeSound方法
        animal2.makeSound(); // 调用的是子类Cat的makeSound方法
    }
}

代码解释:
在这个例子中,Animal是父类,而Dog和Cat是Animal的子类。PolymorphismExample类中,我们创建了一个Animal类型的animal1对象,但实际上将其赋值为Dog类的一个实例。同样地,我们创建了另一个Animal类型的animal2对象,但将其赋值为Cat类的一个实例。
当调用animal1.makeSound()时,由于animal1引用的实际对象是Dog类的实例,因此会调用Dog类中的makeSound方法,并输出"狗发出汪汪声"。
当调用animal2.makeSound()时,由于animal2引用的实际对象是Cat类的实例,因此会调用Cat类中的makeSound方法,并输出"猫发出喵喵声"。
这就是Java多态性的体现,通过父类引用指向不同子类对象,根据实际对象的类型来自动调用相应的方法实现。

类型转换

数据类型精度从小到大的顺序为:
byte < short < int < long < float < double<boolean<char

  1. 隐式转换(自动转换):把小范围的数据类型赋值给大范围的数据类型
int a = 123;
double b = a; // 隐式类型转换
System.out.println(a); // 输出:123
System.out.println(b); // 输出:123.0
  1. 显式转换(强制转换):把大范围的数据类型赋值给小范围的数据类型
double a = 123.456;
int b = (int) a; // 显式类型转换
System.out.println(a); // 输出:123.456
System.out.println(b); // 输出:123

向上转型
作用是:提高程序的扩展性

class Animal{
     abstract void eat();
}
class Cat extends Animal{
      void look() {
	System.out.println("看家");
	 }
    }      
   Animal x=new Cat()  //向上造型,Cat对象提升到Animal对象
   x.eat()   //只能使用父类中的方法
   x.look()  //报错!不能使用子类中的方法

向下转型
作用是:为了使用子类中的特有方法

class Animal{
     abstract void eat();
}
class Cat extends Animal{
      void look() {
		System.out.println("看家");
	    }
    }      
	Animal x=new Cat()
	Cat  m=(Cat)x;  //向下转型
  		 m.eat() ;
  		 m.look();//子父类中的方法都可以使用

数组

求类型为字符串String的数组长度时有括号length(),而求整型int的数组长度时没有括号length

数组迭代的两种方式:

  1. for循环
	for(int i =0;i<a.length;i++) {
		System.out.println(a[i]);
	}
  1. 增强for循环
	//for(数据元素的类型 临时变量的名称 : 数组的名字){}
	for(int x:a) {
		System.out.println(x);
	}

数组的copy:

数组拷贝的方法在System类中
		//System.arraycopy(始, 始下标 , 终, 终下标, 允许覆盖位数);
		int a1[]={1,2,3};
		int b2[]={4,5,6,};
		System.arraycopy(a1, 1, b2, 1, 2);
		for (int i : b2) {
			System.out.println(i);
		}
		//输出结果为:4 2 3

运算符

  • 单目运算符:单数的+(正)、-(减)、++(自增)、–(自减)
  • 双目运算符:两数的加减乘除取余
  • 三目运算符:a>b?true:false

移位运算符(先转换为二进制再进行运算):<< 、>>(有符号右移)、>>>(无符号右移),左移一位相当于×2,右移一位相当于÷2,移两位就乘或除4(效率比用乘除号高)

int a =8,c;
c = a>>2;
//原来为 0000 1000
//移动后为 0000 0010

逻辑运算符

&&和&、||和| 的区别:

  • (一假则假)&&与&的运算结果是相同的,对于&而言无论左边为什么值,右边都参与运算;对于&&来说,只要左边为false右边就不再运算,直接返回false
  • (一真则真)||与|的运算结果是相同的,对于|而言无论左边为什么值,右边都参与运算;对于||来说,只要左边为true右边就不再运算,直接返回true
  • 这叫短路现象,通常使用&&和 || 效率较高

空指针异常

Java会出现空指针异常(NullPointerException)是因为代码中使用了一个空对象,而这个空对象没有进行初始化或者已经被释放了。

在Java中,每个对象变量都存储着对对象的引用,如果该引用值为null,就表示该变量不指向任何对象。当我们调用一个空对象的方法或属性时,在编译时并不会报错,但在运行时程序会抛出空指针异常。

例如,下面代码中的str变量未初始化,它的值为null,如果我们尝试调用它的length()方法,就会抛出空指针异常:

String str = null;
int len = str.length(); // 这里会抛出空指针异常

这个异常在Java编程中比较常见,因为我们在代码中很容易忽略一个对象是否为空,而直接进行属性和方法的调用。因此,我们在写Java程序时,应该始终意识到对象可能为空,并编写相应的代码来避免空指针异常的发生。

解决办法: 可以在使用对象之前先进行非空判断,以确保对象不为空;或者在创建对象时进行初始化,以避免变量的值为空。另外,也可以使用try-catch语句来捕获空指针异常,从而保证程序的正常运行。

以下例子不会出现空指针异常:

class Person {
 static void sayHello() {
  System.out.println("HelloWorld!");
 }
}
public class Example {
 public static void main(String[] args) {
  ((Person) null).sayHello();
 }
}

代码解释: 由于静态方法sayHello()是与类关联而不是对象关联的,所以它可以直接通过类名调用,而不依赖于具体的对象实例。在这段代码中,尽管 (Person) null 强制转换的结果是一个空对象引用,但由于 sayHello() 方法是静态的,它不需要访问或操作任何对象的特定状态,因此不会导致空指针异常。相反,它只是简单地在标准输出中打印了 “HelloWorld!” 字符串。需要注意的是,在其他情况下,如果调用一个非静态方法时使用了空对象引用,就会抛出空指针异常。这是因为非静态方法需要一个有效的对象实例来执行,而空对象引用没有与之关联的对象,无法执行非静态方法。

修饰符

static静态修饰符

  • 修饰属性时,使该属性在类中共有,操作时都是在该属性上操作
  • 修饰方法时,和上面差不多,只不过有时候要创建对象来调用该方法
  • 静态方法可以直接调用,而不需要再另外创建实例化对象,如果该方法不是静态的,则还需要创建实例化对象才能调用该方法。
  • static方法中不能有用this调用的方法

final修饰符

  • 修饰属性:定义就必须直接赋值或者在构造方法中进行赋值,并且后期都不能修改
  • 修饰方法:定义必须有实现代码,不可被子类重写,但是可以被重载
  • 修饰类:不能被定义为抽象类或是接口,不可被继承;修饰的类可以是一个抽象类的子类,但是这个子类不能定义任何抽象方法
  • 修饰的代码块在方法中可以保证只被执行一次(和代码块被static修饰的效果一样)

需要注意的是: final 修饰的变量可以是基本类型或引用类型。对于基本类型,一旦被赋值后就不能再修改其值;对于引用类型,一旦被赋值后就不能再指向其他对象,但是该对象的属性值是可以被修改的。修饰的变量可以是全局变量或局部变量,全局变量需要在声明时赋值,而局部变量则可以在方法中任何位置赋值

final关键字的使用场景如下:

  • 当一个类不希望被继承时

  • 当一个方法不希望被子类重写时

  • 当一个变量在定义后不希望被修改时

  • 当一个变量在多线程环境下需要保证线程安全时,可以将该变量声明为final,因为final变量在多线程环境下是不可变的,所以不会出现线程安全问题

     属性都有默认值:整型为0,浮点型是0.0,布尔型为false,字符型或引用类型都是null,但局部变量不被自动初始化,必须手动初始化。
    

关联和依赖关系

关联关系也是“has”的关系
分为单向和双向关联(一个类做为另一个类的属性类型存在)

	//单向关联
     public class Phone {
     	 private  Person per;
     }
     //如果下面的也有那就是双向关联
     public  class Person {
    	 private Phone phone;
	}

还分为一对一和一对多关联
解决一对多可以使用集合或数组:

	//集合
    public class Classes{     
     }
    public  class Student{
    private List Classess}
	//数组
    public class Classes{     
    }
    public  class Student{
    private Classes[] Classess}

如果两个互相关联的类中有整体和部分的关系,关联关系分为: 聚合和组合,主要区别在于生命周期不同。

聚合:创建Team对象时Player对象可以不创建,当Player 对象销毁时Team还没销毁

    public class Team{
      private Player  player;//运动员
     }
    public  class Player{
     }

组合(绑定):创建Team对象的时Player类同样创建, Team对象销毁时,Player对象也销毁

public class Team{
     private Player p=new Player();//队员
     }
     class Player{
     }

依赖关系是“use”的关系:指一个类A使用到了另一个类B
表现为类B作为参数被类A在某个method方法中使用,例如:

   public  class Person {
   public void travel(Bus bus){
  	 }
   }

super关键字

public class A extends B{
	A(){
		super();  //调用父类的构造方法,一定要放在方法的首个语句
	}
}

抽象

  • 概念:不能具体描述对象的类,如形状是抽象的类,圆、三角形等是具体的类

如果类中有抽象方法,则该类必须定义成抽象类,但抽象类中不一定有抽象方法,抽象类不能被实例化

抽象类只能用作基类(父类),表示的是一种继承关系。继承抽象类的非抽象类必须实现其中的所有抽象方法,而已实现方法的参数、返回值要和抽象类中的方法一样。否则,该类也必须声明为抽象类。

  • 构造方法和静态方法不可以修饰为abstract
  • 在类中没有方法体的方法,就是抽象方法,例如:
    public abstract void draw(); //没有花括号

抽象类的作用: 抽象类主要用来进行类型隐藏;也就是使用抽象的类型来编程,但是具体运行时就可以使用具体类型。能够在开发项目中创建扩展性很好的架构,优化程序。

编译和运行的区别

编译(Compile):编译是将高级语言代码转换为计算机能够理解和执行的低级语言(如机器码或字节码)的过程。编译器会对源代码进行语法检查、语义分析和优化,最终生成可执行的目标代码或中间代码。在编译阶段,程序员可以发现并解决一些潜在的问题,如语法错误、类型错误等。编译后的代码可以在稍后的时间内重复运行,而无需重新编译。

运行(Run):运行是指执行已经编译好的程序的过程。在运行时,计算机会加载并执行编译生成的目标代码或中间代码。程序被加载到计算机内存中,按照指定的执行顺序逐行执行。在运行过程中,程序会与外部环境进行交互,接收输入并产生输出。运行阶段是将代码转化为实际结果、实现预期功能的过程。

instanceof关键字

用法:result = 对象名称 instanceof 类型
说明:如果对象是这个类型的一个实例,则 instanceof 运算符返回 true。如果对象不 是指定类的一个实例,或者对象是 null,则返回 false
示例:

public class Animal {
    // Animal类的定义
}
public class Dog extends Animal {
    // Dog类继承自Animal类
}
public class Cat extends Animal {
    // Cat类继承自Animal类
}
public class Example {
    public static void main(String[] args) {
        Animal animal = new Dog(); // 创建一个Dog对象并赋值给Animal类型的变量
        if (animal instanceof Dog) {
            System.out.println("animal是Dog类或其子类的实例");
        } else {
            System.out.println("animal不是Dog类或其子类的实例");
        }
        
        Animal anotherAnimal = new Cat(); // 创建一个Cat对象并赋值给Animal类型的变量
        if (anotherAnimal instanceof Dog) {
            System.out.println("anotherAnimal是Dog类或其子类的实例");
        } else {
            System.out.println("anotherAnimal不是Dog类或其子类的实例");
        }
    }
}

其输出结果为:

animal是Dog类或其子类的实例
anotherAnimal不是Dog类或其子类的实例

解释说明:在示例中,animal对象是Dog类型的实例,因此animal instanceof Dog的判断结果为true;而anotherAnimal对象是Cat类型的实例,所以anotherAnimal instanceof Dog的判断结果为false。通过使用instanceof关键字,可以在Java中方便地判断对象的类型和继承关系。

JDK与JRE的区别

JDK是用于开发Java应用程序的工具包,而JRE是用于运行Java应用程序的运行时环境。JRE包含了Java虚拟机(JVM)和Java核心类库等运行时必需的组件,而JDK则在此基础上提供了开发所需的编译器和开发工具等

Object

Object是所有类的根,包括数组

Object和Object[]之间的区别:
方法中的形参是Object类型时,任何类型的参数都可以传进去执行。
方法中形参是Object[]类型时,只有对象数组可以传入执行。

public  static void arrayTest(Object[] obj){
}    
public static  void main(){
   int[] array=new  int[4]; 
   arrayTest(array)//错误出现,因为array不是对象型的
   // 换成 Array [] array=new Array[4]; 才对,其中Array是一个类,是一个对象型的
}

hashCode方法
获取对象的哈希码值,为16进制

equals方法与hashCode方法关系:
如果两个对象使用equals比较返回true,那么它们的hashCode值一定要相同
如果两个对象equals比较返回false,那么它们的hashCode值不一定不同

例题
以下代码会输出size:4

class RectObject {
 public int x;
 public int y;
 
 public RectObject(int x, int y) {
  this.x = x;
  this.y = y;
 }
 @Override
 public int hashCode() {
  // TODO Auto-generated method stub
  return (int)System.nanoTime();
 }
 @Override
 public boolean equals(Object obj) {
  return false;
 }
}
public class Example {
 public static void main(String[] args) {
  HashSet<RectObject> set = new HashSet<RectObject>();
  RectObject r1 = new RectObject(3, 3);
  RectObject r2 = new RectObject(5, 5);
  RectObject r3 = new RectObject(3, 3);
  set.add(r1);
  set.add(r2);
  set.add(r3);
  set.add(r1);
  System.out.println("size:" + set.size());
 }
}

解释说明: HashSet不会将完全相同的元素重复添加进去,RectObject类重写了hashCode()方法,使其返回每次调用都会产生不同的哈希码。这样每个RectObject对象的哈希码都不同。当向HashSet中添加元素时,它首先会通过hashCode()方法确定元素应该存储的位置。然后它会使用equals()方法来判断是否存在相同的元素。在这里,RectObject类重写的equals()方法总是返回false,意味着所有的RectObject对象都被视为不相等。因此,尽管r1、r2和r3的坐标值相同,但由于它们的哈希码不同,HashSet将把它们视为不同的元素,并将它们都成功添加到集合中。同时,由于HashSet不允许重复元素,重复添加的r1也会被忽略。最终,HashSet中包含了四个元素:r1、r2、r3和第二次添加的r1。因此,输出"size:4"。

而以下代码会输出 size:3

class RectObject {
 public int x;
 public int y;

 public RectObject(int x, int y) {
  this.x = x;
  this.y = y;
 }
 @Override
 public boolean equals(Object obj) {
  if (this == obj)
   return true;
  if (obj == null)
   return false;
  if (getClass() != obj.getClass())
   return false;
  final RectObject other = (RectObject) obj;
  if (x != other.x) {
   return false;
  }
  if (y != other.y) {
   return false;
  }
  return true;
 }
}
public class Example {
 public static void main(String[] args) {
  HashSet<RectObject> set = new HashSet<RectObject>();
  RectObject r1 = new RectObject(3, 3);
  RectObject r2 = new RectObject(5, 5);
  RectObject r3 = new RectObject(3, 3);
  set.add(r1);
  set.add(r2);
  set.add(r3);
  set.add(r1);
  System.out.println("size:" + set.size());
 }
}

解释说明:RectObject类中,equals()方法被重写为比较两个对象的xy坐标是否相等。如果两个对象的坐标相等,则被视为相等。set.add(r1)set.add(r2)set.add(r3)分别将对象r1r2r3添加到HashSet集合中。由于r1r3具有相同的坐标值(都是(3, 3)),根据equals()方法的实现,它们被视为相等的对象,所以只会添加一个到集合中。因此,最终输出的结果是"size:3",表示集合中有3个不同的RectObject对象。需要注意的是,HashSet类在判断对象是否相等时首先会调用hashCode()方法,以确定对象属于哪个槽位,然后再调用equals()方法进行进一步的比较。在这段代码中,由于没有重写hashCode()方法,HashSet会使用默认的hashCode()实现,它基于对象的内存地址生成哈希码。因此,即使两个对象具有相同的坐标值,它们的哈希码由于不同的内存地址而不同,不会被视为相等的对象。但由于equals()方法的实现,它们在HashSet中会被认为是相等的。

接口

接口是设计层面的概念,往往由设计师设计,将定义与实现分离
程序员实现接口,实现具体方法
面向接口编程的意思是指在面向对象的系统中所有的类或者模块之间的交互是由接口完成的
  • 接口就是一个特殊的抽象类,接口中的方法都是抽象方法
  • native关键字表明修饰的方法是由其他非Java语言编写的
  • 接口中定义的方法默认是public和abstract的,不能被private或protected修饰
  • 接口可以继承多个接口,而类只能继承一个类

类实现接口,本质上与类继承类相似,区别在于“类最多只能继承一个类,即单继承,而一个类却可以同时实现多个接口”,多个接口用逗号隔开即可。实现类需要覆盖(重写)接口中的所有抽象方法,否则该类也必须声明为抽象类。接口是抽象的,接口中没有任何具体方法和变量,所以接口不能进行实例化

异常

基本语法

  try{
	  可能会发生异常的代码
	  //有return语句finally也会执行,除非强制关闭虚拟机(System.exit(0))
  }catch(异常类型 引用名){
	  异常处理代码
  }finally{
 	  最后都必须执行的代码
  }

异常与错误的区别:
异常是程序中发生的不正常事件流,通过处理程序依然可以运行下去。但是错误是无法控制的,程序肯定要中断
Error类和Exception类都是Throwable类的子类

例子

对以下两个代码片段说法正确的是?
代码片段1int a = 3;
 int b = 0;
 int c = a / b;
代码片段2float a = 3.0f;
float b = 0.0f;
float c = a / b;
只有代码片段1抛出异常

解释说明: 代码1会抛出 ArithmeticException 算术异常,原因是在整数除法操作中,除数为0会引发该异常。而代码2,在Java中,浮点数除以0不会引发异常,而是会返回一个特殊的值Infinity(正无穷大)、-Infinity(负无穷大)或NaN(不是一个数字)。当变量b被赋值为0.0f时,执行a / b操作时会得到正无穷大(Infinity)的结果。所以,变量c将被赋值为正无穷大。请注意,Java中整数(取余)取模0会得到一个结果为0的整数,而浮点数取模0则会引发异常,任何包含NaN的比较操作都会返回false

  • 正无穷大(Infinity):当被除数为正数而除数为0时,结果为正无穷大
  • 负无穷大(-Infinity):当被除数为负数而除数为0时,结果为负无穷大
  • NaN(不是一个数字):当被除数为0而除数也为0时,结果为NaN

throws声明异常,throw抛出异常

  • 任何方法都可以使用throws关键字声明(在方法括号后)异常类型,包括抽象方法
  • 子类覆盖父类中的方法,子类方法不能声明抛出比父类范围更大的异常
  • 使用了throws的方法,调用时必须处理声明的异常,要么使用try-catch,要么继续使用throws声明

层层抛出异常
catch语句中,处理异常后,再次用throw抛出该异常对象
继续抛出异常的作用:使得调用该方法的方法,能够再一次捕获并处理异常

自定义异常

  • 自定义异常就是自己定义的异常类,也就是API中的标准异常类的直接或间接的子类
基本语法
public  class 异常类名 extends Exception{
        public 异常类名(String msg){
            super(msg);//使用 super() 方法来调用 Exception 类的构造函数以确保 MyException 类继承了 Exception 类的所有属性和行为
	 }
 }

示例
在这里插入图片描述

  • Math类中的方法都是static方法,调用静态方法可以直接使用类名.方法名来调用,不需要先创建类的一个对象

泛型

泛型类
Java 中的泛型是一种在编译时进行类型检查和类型安全的机制。它允许在定义类、接口或方法时使用参数化类型,以便在使用时指定具体的类型。泛型使得代码更加灵活、可重用,并提供了更好的类型安全性。

泛型的主要目的是在编译时捕获类型错误,以避免在运行时出现类型转换异常或其他类型相关的错误。

下面是一个使用泛型的简单示例:

public class Box<T> {
    private T content;

    public T getContent() {
        return content;
    }
    public void setContent(T content) {
        this.content = content;
    }
}
public class Main {
    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<Integer>();
        integerBox.setContent(123);
        int value = integerBox.getContent();
        System.out.println(value);  // 输出:123
        
        Box<String> stringBox = new Box<String>();
        stringBox.setContent("Hello");
        String strValue = stringBox.getContent();
        System.out.println(strValue);  // 输出:"Hello"
    }
}

在这个示例中创建了一个名为 Box 的泛型类。通过在类名后面用尖括号 <> 指定泛型参数 T,我们可以在类中使用这个参数作为类型的占位符。

Box 类中,我们使用了泛型参数 T 来定义 content 字段的类型,并在 getContent()setContent() 方法的返回类型和参数类型中使用了泛型参数 T

main() 方法中,我们创建了两个 Box 对象,一个是 Box<Integer> 类型,另一个是 Box<String> 类型。这样就分别指定了 content 字段的类型为 IntegerString

通过使用泛型,我们可以在编译时确保内容的类型安全性。如果我们尝试将错误类型的值放入 Box 对象中,编译器将会提供错误提示。

总之,Java 中的泛型是一种在编译时进行类型检查和类型安全的机制。它允许在定义类、接口或方法时使用参数化类型,并在使用时指定具体的类型,从而提供更好的代码复用性和类型安全性

泛型接口

与泛型类完全相同
Comparable接口是泛型接口

public interface Comparable<T> { 
public boolean compareTo(T other);
}

Comparable 接口包含一个类型参数 T,该参数是一个实现 Comparable 的类可以与之比较的对象的类型。这意味着如果定义一个实现 Comparable 的类,比如 String,要声明它可与什么比较(通常是与它本身比较)

public class String implements Comparable<String> { ... }

泛型方法

注意: 是否拥有泛型方法,与其所在的类是否泛型没有关系。要定义泛型方法,只需将泛型参数列表置于返回值前。

下面是一个使用泛型方法的示例:

public class ArrayUtils {
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.println(element);
        }
    }
}
public class Main {
    public static void main(String[] args) {
        Integer[] integerArray = {1, 2, 3, 4, 5};
        ArrayUtils.printArray(integerArray);

        String[] stringArray = {"Hello", "World"};
        ArrayUtils.printArray(stringArray);
    }
}

在这个示例中创建了一个名为 ArrayUtils 的工具类,并在其中定义了一个名为 printArray() 的泛型方法。

在泛型方法的定义中,我们使用尖括号 <T> 来指定泛型参数,并在方法参数和方法体中使用该泛型参数来表示方法的参数类型和变量类型。

printArray() 方法中,我们使用 for-each 循环遍历传入的数组,并将数组中的元素逐个打印出来。

main() 方法中,我们首先创建一个 Integer 类型的数组 integerArray 和一个 String 类型的数组 stringArray。然后,分别调用 ArrayUtils 类中的 printArray() 方法,并传入对应的数组作为参数。

通过使用泛型方法,我们可以在编译时对传入的数组进行类型检查,并且避免了为不同类型的数组编写重复的打印方法。

总之,泛型方法是定义在类中的方法,使用泛型参数来增加方法的灵活性和通用性。它可以独立于类的泛型参数,使得方法可以接受不同类型的参数,并提供更好的代码复用性和类型安全性。

集合

Collection 所有集合类的根接口,还是一个泛型接口,子接口有List、Set、Map、Queue(队列)等

List 的实现类及使用场景:

  • ArrayList 基于数组实现的动态数组,可以根据索引直接访问元素,访问速度快。适用于频繁访问和遍历元素的场景,但不适用于频繁插入和删除元素的场景
  • LinkedList 基于双向链表实现的集合,每个元素都包含一个前驱节点和一个后继节点。适合在需要频繁在列表的头部和尾部插入和删除元素的情况下使用,但访问和遍历元素的效率较低。
  • Vector 是 jdk1.0中的集合,与ArrayList类似,但它是线程安全的动态数组,所有的方法都是同步的。适用于多线程环境下需要保证线程安全的场景

Set 的实现类及使用场景:

  • HashSet 是基于哈希表实现的,不保证元素的顺序,Set都不允许存储相同的对象;它使用哈希函数来计算元素的存储位置,可以快速添加、删除和查找元素。适用于需要快速添加、删除和查找元素,并且不需要保持元素的顺序的场景
  • LinkedHashSet 是在HashSet的基础上使用链表来保持元素的插入顺序,支持快速添加、删除和查找元素,同时保持元素的插入顺序。适用于需要快速添加、删除和查找元素,并且需要保持元素的插入顺序的场景
  • TreeSet 是基于红黑树实现的有序集合,可以按照元素的自然顺序或者指定的比较器进行排序,支持快速添加、删除和查找元素,同时保持元素的有序性;它要求元素必须实现 Comparable接口,或者在创建TreeSet时指定比较器。适用于需要快速添加、删除和查找元素,并且需要保持元素的有序性的场景

Map 的实现类及使用场景:

  • HashMap 是基于哈希表实现的Map,它使用键的hashCode值来确定存储位置,是无序无重复的;允作空键空值,但空键只有一个;键不可以重复,但值能重复;具有快速的插入、删除和查找操作。在JDK1.8中,HashMap采用数组+链表+红黑树(一种平衡搜索二叉树)实现,当链表长度超过阈值8时,将链表转换为红黑树。适用于需要快速插入、删除和查找键值对,并且不关心元素的顺序的场景
  • LinkedHashMap 是基于哈希表和双向链表实现的,是HashMap的一个子类,它在HashMap的基础上维护了一个双向链表,用于保持插入顺序或者访问顺序;也允作空键空值。适用于需要保持元素的插入顺序或者访问顺序,并且需要快速插入、删除和查找键值对的场景
  • TreeMap 是基于红黑树实现的,保持了键的有序性,可以按照键的自然顺序或者指定的比较器进行排序;不允许使用null作为键,但可以使用null作为值。适用于需要保持元素的有序性,并且需要快速插入、删除和查找键值对的场景
  • ConcurrentHashMap 是线程安全的HashMap的实现,它通过分段锁(Segment)来实现并发访问的效率,允许多个线程同时访问,不会阻塞其他线程;不保证元素的顺序,即不保证遍历顺序与插入顺序一致;与它功能类似但略差的还有HashTable,但已被淘汰。适用于需要在多线程环境下进行插入、删除和查找操作,并且不关心元素的顺序的场景

Map中保存的是键值对 Map<key,Value> ,Key值不允许重复,如果重复,则覆盖。常用方法有:

  • put(K key,V value)该方法可以将key和value存到Map对象

  • get(Object key)该方法可以根据key值返回对应的value

  • size()返回Map对象中键值对的数量

     补充:session 的底层是 Map
    
  • Iterator 遍历集合的迭代接口

在这里插入图片描述
图中的虚线框表示接口,不能new,粗黑框表示类,可以new

ArrayList例子:

public class User {
 private String userName

 public User(String userName) {
   super();
   this.userName = userName;
  }
 public String getUserName() {
    return userName;
  }
       ……
public void setUserName(String userName){      
   this.userName = userName;
  }
}

Test:
public class GenericsList {
public static void main(String[] args) {
    //创建用户对象
     User user=new User("张三");
     User user1=new User("李四"); 
    //创建集合对象,存放用户对象
    List<User> userList=new ArrayList<User>(); //用泛型
   userList.add(user);
   userList.add(user1);
}
}

三种循环遍历

	public static void main(String[] args) {
		// 创建泛型集合
		ArrayList<Book> list=new ArrayList<Book>();
		
		Book b1=new Book("JAVA",24);
		Book b2=new Book("C",54);
		//添加数据
		List.add(b1);
		List.add(b2);
		//获取数据
		System.out.println(List.get(1));
		
		//for循环遍历
		for (int i = 0; i<list.size(); i++) {
			System.out.println(List.get(i));
		}
		//增强for循环遍历
		for (Book book : list) {
			System.out.println(book);
		}
		//Iterator遍历
		Iterator<Book> iter=list.iterator();
		while(iter.hasNext()) {
			System.out.println(iter.next());
		}
	}

以下例子主要解释说明:使用 array.add(0, stu1) 将 stu1 添加到集合的索引位置0上,然后使用 array.add(0, stu2) 将 stu2 添加到索引位置0上。这样做会导致 stu2 存储在索引位置0上,而 stu1 存储在索引位置1上,因为后添加的对象会将已有的对象后移。

public class ArrayListTest {

	public static void main(String[] args) {
		List<Student> array = new ArrayList<Student>();// 创建集合存放学生对象
		Student stu1 = new Student();// 创建学生对象
		stu1.setName("王星");
		Student stu2 = new Student();// 创建学生对象
		stu2.setName("王依");
		array.add(0, stu1);// 添加stu1对象
		array.add(0, stu2);// 添加stu2对象
		
		System.out.println(array.size());// 打印集合中的元素个数
		array.remove(0);// 移除索引位置0上的元素
		System.out.println(array.size());// 打印集合中的元素个数
		array.remove(stu1);//根据对象移除集合中的元素
		System.out.println(array.size());// 打印集合中的元素个数
	}
}

模拟登录判断例子:

package com.cwl.study.test;

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

import com.cwl.study.User;

public class LoginTest {
	public static void main(String[] args) {
		//创建集合存放对象
		List<User> userlist=new ArrayList<User>();
		User user1=new User("小明",123456);
		User user2=new User("小陈",123456);
		User user3=new User("小刚",123456);
		User user4=new User("小哈",654321);
		userlist.add(user1);
		userlist.add(user2);
		userlist.add(user3);
		userlist.add(user4);
		
		// 创建控制台输入对象
		Scanner sc=new Scanner(System.in);
		int m=0;
		while(m<3) {
			System.out.println("请输入用户名:");
			String nowuser=sc.next();  //接收控制台用户名
			System.out.println("请输入密码:");
			int nowpwd=sc.nextInt();  //接收控制台密码,是基本数据类型
			
			for (int i = 0; i < userlist.size(); i++) {
				User indexuser=userlist.get(i);
				// 判断用户名密码是否正确,注意基本数据类型的比较只能用==而不能用equals
				if (nowuser.equals(indexuser.getUserName())&&nowpwd==indexuser.getUserPassword()) {
					System.out.println("登录成功");
					return;
				} 
			}
			System.out.println("用户名或密码错误!请重新输入:");
			m++;
			if (m==3) {
				System.out.println("三次机会已用完,请稍后再试");
			}
		}
	}
}

Java 中的基本数据类型不能直接调用对象方法,包括 equals 方法。基本数据类型有其对应的包装类(wrapper class),比如 int 对应的包装类是 Integer。包装类是对象,可以调用对象方法,例如 equals 方法。如果想要比较两个基本数据类型的值是否相等,应该使用双等号(==)进行比较,而不是使用 equals 方法。例如,使用 == 进行比较两个 int 类型的值:

int num1 = 5;
int num2 = 5;
if (num1 == num2) {
    // 逻辑处理
}

如果需要使用 equals 方法比较两个基本数据类型的值,需要先将其转换为对应的包装类对象,再进行比较。

int num1 = 5;
int num2 = 5;
Integer wrapperNum1 = Integer.valueOf(num1); // 将 int 转换为 Integer
Integer wrapperNum2 = Integer.valueOf(num2);
if (wrapperNum1.equals(wrapperNum2)) {
    // 逻辑处理
}

请注意,在代码中尽量避免将基本数据类型用作对象来进行比较,直接使用 == 进行比较更为高效和简洁。

hashSet和TreeSet的使用和区别:
Humans实体类

//public class Humans{}  //HashSet
public class Humans implements Comparable<Humans>{ //TreeSet
	private String name;
	private String sex;
	private Integer age;
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getSex() {
		return sex;
	}
	public void setSex(String sex) {
		this.sex = sex;
	}
	public Integer getAge() {
		return age;
	}
	public void setAge(Integer age) {
		this.age = age;
	}
	public Humans(String name, String sex, Integer age) {
		super();
		this.name = name;
		this.sex = sex;
		this.age = age;
	}
	public Humans() {
		super();
		// TODO Auto-generated constructor stub
	}
	@Override
	public String toString() {
		return "Humans [name=" + name + ", sex=" + sex + ", age=" + age + "]";
	}
	@Override
	public int hashCode() {
		return Objects.hash(age, name, sex);
	}
	//重写equals方法去重,不重写则默认是进行虚地址比较,重写则是对属性进行比较
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Humans other = (Humans) obj;
		return Objects.equals(age, other.age) && Objects.equals(name, other.name) && Objects.equals(sex, other.sex);
	}
	//重写compareTo方法,实现降序排序等
	@Override
	public int compareTo(Humans h) {
		if (age>h.age) {
			return -1;
		} else if(age<h.age){
			return 1;
		}else {
			return 0;
		}
	}
}

测试类

public class HumanTest {

	public static void main(String[] args) {
//		Set<Humans> humanSet=new HashSet<Humans>();  //HashSet需要重写equals方法
//		Humans humans1=new Humans("小明","男",18);
//		Humans humans2=new Humans("小陈","女",20);
//		Humans humans3=new Humans("小明","男",18);
//		
//		humanSet.add(humans1);
//		humanSet.add(humans2);
//		humanSet.add(humans3);
//		
//		for (Humans humans : humanSet) {
//			System.out.println(humans);
//		}
		
		Set<Humans> humanSet=new TreeSet<Humans>();  //而TreeSet需要实现Comparable接口
		Humans humans1=new Humans("小明","男",18);  //并重写compareTo方法,实现降序排序等逻辑,默认按照字典顺序排序
		Humans humans2=new Humans("小陈","女",20);
		Humans humans3=new Humans("小明","男",18);
		
		humanSet.add(humans1);
		humanSet.add(humans2);
		humanSet.add(humans3);
		
		for (Humans humans : humanSet) {
			System.out.println(humans);
		}
	}
}

HashMap和TreeMap的使用和区别:

HashMap中元素的key值不能重复,即彼此调用equals方法,返回为false。排列顺序是不固定的。
TreeMap中所有的元素都保持着某种固定的顺序,如果需要得到一个有序的Map就应该使用TreeMap,key值所在类必须实现Comparable接口。

HashMap的常用方法(TreeMap的也类似)

 put<key,value>  —>存放对象
 get(key);       —>获取key所对应的value值的数据
 keySet()        —> 返回此映射中所包含的键的 set 视图。

HashMap的例子:

 import java.util.HashMap;
 public class HashMapTest {
public static void main(String[] args) {
      User user1=new User("王敏");
      User user2=new User("王辉");
      HashMap<String,User> map=new HashMap<String, User>();
      map.put(001", user1);
      map.put(002", user2);
	}
}

TreeMap的例子:
它适用于按自然顺序或自定义顺序遍历键(key)。TreeMap根据key值排序,key值(类)需要实现Comparable接口,实现compareTo方法。TreeMap根据compareTo的逻辑,对key进行排序。

ScortInfo实体类:
public class ScortInfo implements Comparable<ScortInfo> {
		private int num;
public int getNum() {
	return num;
	}
public void setNum(int num) {
	this.num = num;
	}
@Override
public int compareTo(ScortInfo o) {
	Return new  Integer(this.num).compareTo(o.getNum());
	}
public ScortInfo(int num) {
	this.num = num;
	}
}

测试类:
public class TreeMapTest {
public static void main(String[] args) {
	User user1=new User("王敏");
	User user2=new User("王辉");
	TreeMap<ScortInfo,User> tree=new TreeMap<ScortInfo,User>();
	tree.put(new ScortInfo(12), user1);
	tree.put(new ScortInfo(23), user2);
}

Properties类
Properites类是Hashtable类的子类,所以也间接地实现了Map接口。 在实际应用中,常使用Properties类对属性文件进行处理。
常用方法:

load();	加载文件;
getProperty(key);	通过key值获得对应的value值
setProperty(String key,String value)	给properties文件中写值。 

小例子:

public class TestProperties {
	public static void main(String[] args) {
		Properties props = new Properties();
		try {
			props.load(new FileInputStream(new 	File("src/com/chinasofti/ch18/test.properties"))); //加载文件路径
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		System.out.println(props.getProperty("name")); //文件里的属性
		System.out.println(props.getProperty("password"));
		System.out.println(props.getProperty("age"));
	}
}

Collections类
是集合类的工具类,与数组的工具类Arrays类似,定义了大量静态方法,如排序方法
作用:

同步集合对象的方法
对List排序的方法

例子:

public class GenericsList {
public static void main(String[] args) {
	User user4= new User(11); // 创建用户对象
	User user1 = new User(14);
	User user2 = new User(13);
	User user3 = new User(9);
	List<User> userList = new ArrayList<User>(); // 创建集合对象,存放用户对象
	userList.add(user4);
	userList.add(user1);
	userList.add(user2);
	userList.add(user3);
for(User u:userList){
	System.out.println(u.getUserAge());
}
	Collections.sort(userList);//调用排序方法实现升序排序
	//Collections.sort(userList, Collections.reverseOrder()); //降序排序,排序后都存放在原集合中
	//System.out.println(userList);
 for(User u:userList){
	System.out.println(u.getUserAge());
	}
}
运行结果
排序前:11,14,13,9
排序后:9,11,13,14

IO编程

输入: 把数据读到内存中,即input,进行数据的read操作
输出: 从内存往外部设备写数据,即output,进行数据的write操作

File类
它无法对文件里面的具体内容进行操作
示例:

package com.cwl.study.test;
import java.io.File;
import java.io.IOException;
import com.cwl.study.MyFileFilter;
import com.cwl.study.MyFilenameFilter;

public class FileTest {
	public static void main(String[] args) throws IOException {
		// 创建文件对象,  路径要么用“反斜杠/”,要么用“双斜杠\\”
		File file=new File("D:/java/mytest.txt");
		file.createNewFile();
		
		//创建目录对象
		File dir=new File("D:\\java\\test");
		dir.mkdir();
		
		//List()方法遍历所有文件对象名
		String[] fileName=dir.list();
		for (String name : fileName) {
			System.out.println(name);
		}
		
		//ListFiles()方法遍历所有对象
		File[] files=dir.listFiles();
		for (File f : files) {
			System.out.println(f.getAbsolutePath());
		}
		
		//文件名过滤器
		//List(FilenameFilter)方法遍历符合过滤条件的名字(如以.txt结尾的文件)
		//需要新建类(MyFilenameFilter)实现FilenameFilter接口,并重写accept方法
		String[] fileName2=dir.list(new MyFilenameFilter());
		for (String f2 : fileName2) {
			System.out.println(f2);
		}
		
		//List(FileFilter)方法遍历符合过滤条件的名字,传的是文件
		//同样需要实现接口FileFilter并重写accept方法
		File[] file2=dir.listFiles(new MyFileFilter());
		for (File f2 : file2) {
			System.out.println(f2.getAbsolutePath());
		}
	}
}
//新建类实现接口
package com.cwl.study;
import java.io.File;
import java.io.FilenameFilter;

public class MyFilenameFilter implements FilenameFilter {

	@Override
	public boolean accept(File dir, String name) {
		if (name.endsWith(".txt")) {//过滤只要.txt结尾的文件
			return true;
		} else {
			return false;
		}
	}
}

运行结果如下图:
在这里插入图片描述

GUI图形化编程

Swing组件入门,java也可以写前端页面
内部类: 就是类中类,要访问外部类的属性和方法时使用
匿名内部类: 没有类名,一般定义在外部类的某个方法中,使用匿名类来实现事件处理,会使代码更简洁,更灵活。只能使用一次,使用匿名内部类的前提条件是必须继承一个父类或实现一个接口。

务必理解透以下例子:

package com.cwl.study;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.JTextField;

public class Chat {
	//声明需要的组件
	private JFrame frame;
	private JTextField input;
	private JTextArea output;
	private JButton send,quit,clear;
	
	//对组件进行初始化
	public Chat() {
		frame=new JFrame();
		input=new JTextField();
		output=new JTextArea();
		send= new JButton("发送");
		quit=new JButton("取消");
		clear=new JButton("清空");
		
		frame.setTitle("欢迎使用聊天框~");
		frame.setSize(400,300);
		frame.setVisible(true);
	}
	
	//进行布局
	public void init() {
		
		//创建面板并布局
		JPanel panel=new JPanel();
		panel.setLayout(new FlowLayout());
		panel.add(send);
		panel.add(quit);
		panel.add(clear);
		
		Container ContainerPane=frame.getContentPane();
		ContainerPane.setBackground(Color.blue);
		
		ContainerPane.setLayout(new BorderLayout());
		ContainerPane.add(input,BorderLayout.SOUTH);
		ContainerPane.add(output,BorderLayout.CENTER);
		ContainerPane.add(panel,BorderLayout.EAST);
		
		// 注册/关联监听器
		quit.addActionListener(new ChatListener());
		send.addActionListener(new SendLinstener());
		clear.addActionListener(new ActionListener() {	//使用匿名内部类
			@Override
			public void actionPerformed(ActionEvent e) {
				output.setText("");
			}
		});
	}
	
	//使用内部类访问外部类的属性
	public class SendLinstener implements ActionListener {

		@Override
		public void actionPerformed(ActionEvent e) {
			String inputmsg=input.getText();
			output.append("我:"+inputmsg+"\n");  //追加内容
			input.setText("");	//清空内容
		}
	}
	
	//以下内部类可以使用匿名内部类代替,就不用写类名
//	public class ClearLinstener implements ActionListener {
//
//		@Override
//		public void actionPerformed(ActionEvent e) {
//			output.setText("");	//清空内容
//		}
//	}
	
	public static void main(String[] args) {
		Chat chat=new Chat();
		chat.init();
	}
}
实现退出监听器接口
package com.cwl.study;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class ChatListener implements ActionListener {

	@Override
	public void actionPerformed(ActionEvent e) {
		System.out.println("聊天已退出");
		System.exit(0);  // 退出/关闭
	}
}

多线程

创建线程的方式

  • 继承Thread类,并重写run方法
  • 实现Runnable接口,并重写run方法
  • 实现Callable接口,并实现call()方法,通过创建 FutureTask对象并传入Callable对象,调用start()方法启动线程
  • 通过线程池来调度任务(推荐,效率高)

继承Thread类:

定义:
public class MyThread extends Thread {
	public void run() {	//重写
		……
	}
}

调用:
MyThread thread = new MyThread();
thread.start();	//启动线程,只能启动一次,多次启动会抛出异常

实现Runnable接口:

定义:
public class MyThread implements Runnable{
	@Override
	public void run() {
		……
	}
}

调用:
MyThread r = new MyThread();
Thread thread = new Thread(r);	//创建一个线程作为外壳,将r包起来
thread.start();
两者区别:Runnable接口主要是为了解决Java中不允许多继承的问题,可以继承其他类的方法和属性

线程优先级用整数表示,取值范围是1~10,数值越大优先级越高,一般情况下,线程的默认优先级都是5,但是也可以通过setPriority和getPriority方法来设置或返回优先级;

线程同步

同步方法 –-使用synchronized修饰的方法:

访问修饰符 synchronized 数据返回类型 方法名(){ … }

它锁定的是调用这个同步方法的对象。其它线程不能同时访问这个对象中任何一个synchronized方法。

同步语句块 –-只对这个区块的资源实行互斥访问:

synchronized(共享对象名)
{
  被同步的代码段
}

它锁定的是共享对象名对应的当前对象。线程中实现同步块一般是在run方法中添加。

线程同步注意事项:

  • 不要对线程安全类的所有方法都进行同步,只对那些会改变共享资源方法的进行同步。同步块越大,多线程的效率越低

  • synchronized关键字可以修饰方法,也可以修饰代码块,但不能修饰构造器、抽象方法、成员属性等

  • synchronized关键字是不能继承的

  • 无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁。

     toString方法是在使用对象的时候被调用执行的
    

线程通讯

有三个主要方法:
前两个方法要和synchronized一起使用

  • wait()让当前线程等待
  • notify()唤醒等待的单个线程
  • notifyAll()唤醒等待的所有线程

务必理解透以下订票例子:

车票实体

package com.cwl.study.ticket;

import java.util.Objects;

//车票
public class TrainTicket {
	private String trainNo;
	private String seatNo;
	private String date;

	public String getTrainNo() {
		return trainNo;
	}

	public void setTrainNo(String trainNo) {
		this.trainNo = trainNo;
	}

	public String getSeatNo() {
		return seatNo;
	}

	public void setSeatNo(String seatNo) {
		this.seatNo = seatNo;
	}

	public String getDate() {
		return date;
	}

	public void setDate(String date) {
		this.date = date;
	}

	public TrainTicket(String trainNo, String seatNo, String date) {
		super();
		this.trainNo = trainNo;
		this.seatNo = seatNo;
		this.date = date;
	}

	public TrainTicket() {
		super();
		// TODO Auto-generated constructor stub
	}

	@Override
	public String toString() {
		return "TrainTicket [trainNo=" + trainNo + ", seatNo=" + seatNo + ", date=" + date + "]";
	}

	@Override
	public int hashCode() {
		return Objects.hash(date, seatNo, trainNo);
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		TrainTicket other = (TrainTicket) obj;
		return Objects.equals(date, other.date) && Objects.equals(seatNo, other.seatNo)
				&& Objects.equals(trainNo, other.trainNo);
	}
}

车票售卖后台/窗口实体

package com.cwl.study.ticket;
import java.util.ArrayList;

//车票售卖后台/窗口
public class TicketSeller {

	private static ArrayList<TrainTicket> pool = new ArrayList<TrainTicket>();
	static {
		pool.add(new TrainTicket("G复兴号", "520", "2023.8.1"));
		pool.add(new TrainTicket("G复兴号", "521", "2023.8.1"));
		pool.add(new TrainTicket("G复兴号", "522", "2023.8.1"));
		pool.add(new TrainTicket("G复兴号", "523", "2023.8.1"));
		pool.add(new TrainTicket("G复兴号", "524", "2023.8.1"));
	}

	public static TrainTicket sellTicket(TrainTicket ticket) { // 卖票
		TrainTicket t = null;
		synchronized (ticket) { // 进行同步,加锁保护票
			for (TrainTicket t1 : pool) {
				if (t1.equals(ticket)) {
					t = t1;
					System.out.println("正在出票+" + t1);
					try {
						Thread.sleep(3000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					pool.remove(t1);
					break;
				}
			}
		}
		return t;
	}

	public static void returnTicket(TrainTicket ticket) { // 退票
		synchronized (ticket) {
			pool.add(ticket);
		}
	}
}

乘客实体

package com.cwl.study.ticket;

//乘客
public class Passenger {

	private String name;

	public String getName() {
		return name;
	}

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

	public Passenger(String name) {
		super();
		this.name = name;
	}

	public Passenger() {
		super();
	}

	public void buyTicket(TrainTicket ticket) {	//买票
		System.out.println("乘客" + name + "打算买票,信息为:" + ticket);
		Thread t = new Thread(new BuyTicketThread(this, ticket));
		t.start();
	}

	public void returnTicket(TrainTicket ticket) {	//退票
		System.out.println("乘客" + name + "打算退票,信息为:" + ticket);
		Thread t = new Thread(new ReturnTicketThread(this, ticket));
		t.start();
	}

	@Override
	public String toString() {
		return "Passenger [name=" + name + "]";
	}
}

买票线程

package com.cwl.study.ticket;

//买票线程
public class BuyTicketThread implements Runnable {

	private Passenger passenger;
	private TrainTicket ticket;

	public BuyTicketThread(Passenger passenger, TrainTicket ticket) {
		super();
		this.passenger = passenger;
		this.ticket = ticket;
	}

	@Override
	public void run() {
		synchronized (ticket) {
			TrainTicket t = TicketSeller.sellTicket(ticket);
			while (t == null) {
				System.out.println(passenger.getName() + ",很抱歉,票已经售出,请等待候补。");
				try {
					ticket.wait();	//使该线程进入等待状态
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			if (t != null) {
				System.out.println(passenger.getName() + "购票成功,信息为:" + ticket);
			}
		}
	}
}

退票线程

package com.cwl.study.ticket;

public class ReturnTicketThread implements Runnable {

	private Passenger passenger;
	private TrainTicket ticket;

	public ReturnTicketThread(Passenger passenger, TrainTicket ticket) {
		super();
		this.passenger = passenger;
		this.ticket = ticket;
	}

	@Override
	public void run() {
		synchronized (ticket) {
			TicketSeller.returnTicket(ticket);
			System.out.println("乘客" + passenger.getName() + "正在退票中...");
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			ticket.notifyAll();	//唤醒其他所有线程(通知)
			System.out.println("乘客" + passenger.getName() + "退票成功,信息为:" + ticket);
		}
	}
}

测试类

package com.cwl.study.test;

import com.cwl.study.ticket.Passenger;
import com.cwl.study.ticket.TrainTicket;

public class TicketTest {

	public static void main(String[] args) {

		Passenger p1 = new Passenger("小陈");
		Passenger p2 = new Passenger("石敏");

		TrainTicket ticket1 = new TrainTicket("G复兴号", "520", "2023.8.1");
		TrainTicket ticket2 = new TrainTicket("G复兴号", "521", "2023.8.1");

		p1.buyTicket(ticket1);
		p2.buyTicket(ticket1);

		p1.returnTicket(ticket1);
	}
}

线程安全

实现线程安全方法如下:

  • 同步互斥: 通过在方法或代码块上添加synchronized关键字,可以使得多个线程在执行该方法或代码块时互斥,从而保证线程安全;使用ReentrantLock类,通过创建ReentrantLock对象,并使用 lock()和 unlock()方法进行加锁和解锁操作
  • 使用无状态对象: 无状态对象即不包含任何成员变量的对象,无状态对象能保证线程安全,因为它们不会包含任何共享的可变状态
  • 使用不可变对象: 不可变对象包括 immutable对象和值对象,Java中的String、Integer、Float等包装类是不可变对象
  • 使用线程安全的数据结构: 如一些集合类 Vector、ConcurrentHashMap、HashTable等,他们本身就是线程安全的
  • 使用原子类: 如 AtomicInteger、AtomicLong等,它们能保证基本数据类型的变量在多线程下的安全性
  • 使用ThreadLocal类: 它可以让每个线程都拥有自己的变量副本,从而避免多个线程共享变量的问题
  • 使用并发工具类: 如CountDownLatch、CyclicBarrier、Semaphore
  • 使用线程安全的设计模式: 如使用单例模式,通过使用双重检测锁机制,可以实现线程安全的单例模式

网络编程

反射和内省

这两个是IOC的底层

  • 反射主要是用来获取一个类的初始化对象
  • 内省主要是用来获取对应的属性和方法

反射: 可以通过类所在的路径动态地加载类中的所有信息(属性、方法、构造方法)

Class cla=Class.forName("com.cwl.study.Animal");//包含当前类的所有信息的字节码对象,注意C是大写
	//通过字节码对象来获取类中的属性、方法、构造方法
	Field[] fs=cla.getDeclaredFields();  //获取属性
	for (Field field : fs) {
		System.out.println(field);
	}
	Method[] ms=cla.getDeclaredMethods();  //获取方法
	for (Method method : ms) {
		System.out.println(method);
	}
	Constructor[] cs=cla.getDeclaredConstructors(); //获取构造方法
	for (Constructor constructor : cs) {
		System.out.println(constructor);
	}
  • Java反射机制是指在运行时动态地获取类的信息,包括类名、成员变量、方法、注解等,并通过反射调用类的方法和属性
  • 优点是可以访问私有方法和属性,增加了程序的灵活性和可维护性;可以将对象序列化为字节流,以便于存储和传输
  • 缺点是性能较低,因为反射需要动态地获取类信息并调用方法,比直接调用方法的性能要低;由于可以访问私有属性和方法,破坏代码的安全性;代码复杂和难以阅读

要使用Class类的方法,必须先获得Class类的实例,获得Class类实例的常用方法有如下三个:

  • Object类中的getClass方法:适用于通过对象获得Class实例的情况
    任何类都继承到了getClass方法,任意对象可以调用getClass方法获得Class实例
  • 类名.class方式:适用于通过类名获得Class实例的情况
    任何类名加.class即返回Class实例,例如 Class clazz=String.class;
  • Class类的静态方法 forName(String name):适用于通过类型获得Class实例的情况,尤其类名是变量例如:Class.forName(className);

示例说明:

String s="hello";

//使用对象名获得Class实例
Class clazz1=s.getClass();

//使用类名获得Class实例,类名必须是常量
Class clazz2=String.class;

try {
//使用类名获得Class实例,类名可以是变量
	Class clazz3=Class.forName("java.lang.String");
} catch (ClassNotFoundException e)
	e.printStackTrace();
}

内省: 在字节码对象之中直接获取属性以及对应的setter等方法

	BeanInfo bi=Introspector.getBeanInfo(obj.getClass(), obj.getClass().getSuperclass());
	PropertyDescriptor[] pds=bi.getPropertyDescriptors();	//拿到所有的属性详情
	//遍历输出显示属性名、类型、方法
	for (PropertyDescriptor propertyDescriptor : pds) {
		System.out.println(propertyDescriptor.getName());
		System.out.println(propertyDescriptor.getPropertyType().getCanonicalName());
		System.out.println(propertyDescriptor.getWriteMethod());
	}

设计模式

Spring运用了很多设计模式: IOC容器应用工厂设计模式、Bean的管理应用单例设计模式、AOP切面编程应用代理设计模式、HandlerAdapter应用适配器设计模式、不同的HandlerMapping找寻对应的Handler应用策略设计模式、参数的获取以及转换AgurmentResovler应用责任链设计模式、监听器应用了观察者设计模式等

单例模式

概念:指的是一个类只能有一个实例,这样的类被称为单例类,或者单态类,即Singleton Class;Bean就默认为单例模式

单例类的特点:

  • 单例类只可有一个实例
  • 它必须自己创立这唯一的一个实例
  • 它必须给所有其它的类提供自己这一实例

常见两种实现方式:

  • 饿汉式:加载类的时候就马上初始化一个实例
  • 懒汉式:加载类的时候不初始化,当第一次使用实例时才初始化

该类中包括:
构造方法是private权限,保证其他类无法创建该类实例,只能该类自身创建
声明一个static修饰的自身实例,保证该实例永远只是一个
提供一个public方法,返回定义的static自身实例

适配器模式

适配器模式使原本无法在一起工作的两个类能够在一起工作,有一个中间类(变压器)

Spring AOP的增强或通知使用到了适配器模式,Spring MVC中也是用到了适配器模式适配不同的Controller;

常见两种实现方式:

  • 类形式的适配器----使用继承实现适配器模式
  • 实例形式的适配器 ----使用关联实现适配器模式

类形式涉及的成员:

  • 目标(Target)。这就是我们所期待得到的接口。注意,由于这里讨论的是类变压器模式,因此目标不可以是类,而是接口
  • 源(Adaptee)。现有需要适配的接口或类
  • 变压器(Adapter)。变压器类是本模式的核心。变压器把源接口转换成目标接口。显然,这一角色不可以是接口, 而必须是实类

类形式适配器代码示例: Adapter继承Adaptee再实现Target
目标类:

package com.cwl.study.moshi;

public interface Target {	//接口

	void eat();	//与源类Adaptee中的方法相同
	
	void buy();	//目标类中的新方法
}

源类:

package com.cwl.study.moshi;
//源类
public class Adaptee {

	public void eat() {
		System.out.println("调用了源类Adaptee的eat方法");
	}
}

适配器类:

package com.cwl.study.moshi;
//适配器类
public class Adapter extends Adaptee implements Target {	//先继承再实现

	public void buy() {
		System.out.println("调用了适配器Adapter的buy方法");
	}
}

使用/测试类:

package com.cwl.study.moshi;
//使用/测试类
public class TestMoShi {

	public static void request(Target t) {
		t.eat();
		t.buy();
	}
	public static void main(String[] args) {

		request(new Adapter());
	}
}

最后输出为:

调用了源类Adaptee的eat方法
调用了适配器Adapter的buy方法

实例形式涉及的成员:

  • 目标(Target)。这就是我们所期待得到的接口。目标可以是实的或抽象的类
  • 源(Adaptee)。现有需要适配的接口
  • 变压器(Adapter)。变压器类是本模式的核心。变压器把源接口转换成目标接口。 显然,这一角色必须是实类

实例形式适配器代码示例:
目标类:

package com.cwl.study.moshi;

public interface Target {	//接口

	void eat();	//与源类Adaptee中的方法相同
	
	void buy();	//目标类中的新方法
}

源类:

package com.cwl.study.moshi;
//源类
public class Adaptee {

	public void eat() {
		System.out.println("调用了源类Adaptee的eat方法");
	}
}

适配器类:

package com.cwl.study.moshi;
//适配器类
public class Adapter implements Target {

	private Adaptee adaptee;	//关联Adaptee
	
	public Adapter(Adaptee adaptee) {
		super();
		this.adaptee = adaptee;
	}

	//关联后可以直接调用Adaptee中的方法了
	public void eat() {
		adaptee.eat();
	}
	
	//覆盖Targer中的方法
	public void buy() {
		System.out.println("调用了适配器Adapter的buy方法");
	}
}

使用/测试类:

package com.cwl.study.moshi;
//使用/测试类
public class TestMoShi {

	public static void request(Target t) {
		t.eat();
		t.buy();
	}
	public static void main(String[] args) {

		request(new Adapter(new Adaptee()));
	}
}
//输出结果和上面一样

代理模式

代理模式(Proxy Pattern)是一种结构型设计模式,它提供了一个代理类来控制对另一个对象的访问。代理对象可以充当目标对象的接口,以便在不改变客户端代码的情况下,对目标对象进行间接访问和增加额外功能。

代理模式主要包含以下角色:

  • 抽象主题(Subject):定义目标对象和代理对象的共同接口,以便代理对象可以代替目标对象被使用
  • 目标对象(Real Subject):实际执行业务逻辑的对象,是代理对象所代表的真正对象
  • 代理对象(Proxy):持有目标对象的引用,并实现了与目标对象相同的接口,以便对目标对象的访问进行控制,可以在访问目标对象之前或之后添加额外的操作

又分为静态代理和动态代理,静态代理的代理类在编译期生成,而动态代理的代理类在运行时动态生成。动态代理又有JDK代理和CGLib代理两种。静态代理的弊端:代理者做代理的类型单一,代理的方法单一;JDK动态代理的弊端:必须依赖接口,从而有更强的CGLIB代理不依赖接口;Spring的AOP功能就用到了JDK的动态代理和CGLIB代理

静态代理

例子1:

// 定义一个接口,表示远程服务
interface RemoteService {
    void doSomething();
}

// 实现远程服务的具体类
class RemoteServiceImpl implements RemoteService {
    @Override
    public void doSomething() {
        System.out.println("执行远程服务的操作");
    }
}

// 定义代理类,充当远程服务的本地代表
class RemoteServiceProxy implements RemoteService {
    private RemoteService remoteService;

    public RemoteServiceProxy() {
        // 在代理类的构造函数中实例化远程服务对象
        remoteService = new RemoteServiceImpl();
    }

    @Override
    public void doSomething() {
        // 在调用远程服务之前执行额外的逻辑
        System.out.println("执行一些额外的操作");

        // 调用远程服务的方法
        remoteService.doSomething();

        // 在调用远程服务之后执行额外的逻辑
        System.out.println("执行一些其他操作");
    }
}

public class ProxyPatternExample {
    public static void main(String[] args) {
        // 使用代理对象调用远程服务
        RemoteService proxy = new RemoteServiceProxy();
        proxy.doSomething();
    }
}

输出结果:

执行一些额外的操作
执行远程服务的操作
执行一些其他操作

在上述示例中,RemoteService接口表示远程服务,RemoteServiceImpl是具体的远程服务实现类。RemoteServiceProxy作为代理类,实现了RemoteService接口,并在调用远程服务的方法之前和之后执行了一些额外的操作。在main方法中,通过代理对象调用远程服务的方法,代理对象会在调用之前和之后执行额外的逻辑。

例子2:

//火车站的售票窗口  被代理人
public class Station implements Sell {
	public void sellTicket(){
		System.out.println("售出了票!赚钱了");
	}
}
public interface Sell {
	public void sellTicket();
}
//黄牛  代理人
public class HuangNiu implements Sell {
	private Station s;

	public void sellTicket(){
		System.out.println("将人引到售票窗口");
		s.sellTicket();
		System.out.println("抽成!赚到了钱");
	}
	public HuangNiu(Station s) {
		super();
		this.s = s;
	}
}
public class Test {
	public static void main(String[] args) {
		
		Station s= new Station();
		HuangNiu hn=new HuangNiu(s);
		hn.sellTicket();
	}
}

最后运行结果:

将人引到售票窗口
售出了票!赚钱了
抽成!赚到了钱

静态代理的弊端:代理者做代理的类型单一,代理的方法单一(黄牛不能再为大麦网买票)

动态代理(模糊)

请律师例子:

//人
public class Person implements Lawsuit {

	public void justify() {
		// TODO Auto-generated method stub
		System.out.println("我的情况是。。。。。");
	}
}
//金牌代理律师
public class GoldProxy implements InvocationHandler {
	private Object obj;	//所有类型
	@Override
	public Object invoke(Object proxy, Method method, Object[] args)
			throws Throwable {	//泛指所有方法
		
		System.out.println("111");
		method.invoke(obj, args);
		System.out.println("222");
		
		return null;
	}
	public GoldProxy(Object obj) {
		super();
		this.obj = obj;
	}
}
//官司
public interface Lawsuit {	
	public void justify();	//辩解
}

另加测试的大麦网:

public class Dmw implements Sell {
	public void sellTicket() {
		System.out.println("大麦网卖出去了周杰伦的票!");
	}
}

测试类:

public class Test2 {
	public static void main(String[] args) {
		Person p= new Person();	//声明一个人
		GoldProxy gp= new GoldProxy(p);	//请一个律师
		//建立合同
		Lawsuit ls=(Lawsuit)Proxy.newProxyInstance(p.getClass().getClassLoader(), p.getClass().getInterfaces(),gp);
		ls.justify();
		
		//换大麦网也可以
/*		Dmw d=new Dmw();
		GoldProxy gp= new GoldProxy(d);
		Sell s=(Sell) Proxy.newProxyInstance(d.getClass().getClassLoader(), d.getClass().getInterfaces(), gp);
		s.sellTicket();*/
	}
}

JDK动态代理的弊端:必须依赖接口,从而有更强的CGLIB代理不依赖接口

工厂模式

Spring使用工厂模式,通过BeanFactory和ApplicationContext来创建对象

观察者模式

主要思想:有实时监听,这边有改动,另外一边也会有相应的改变

Spring事件驱动模型就是观察者模式的一个经典应用;Spring事件驱动模型是Spring框架中的一种编程模型,也被称为发布/订阅模型,通过使用观察者模式和事件机制,实现了组件之间基于事件的解耦和通信

装饰模式

策略模式

责任链模式

时间戳

System.currentTimeMillis() 是Java中的一个方法,它用于获取当前系统时间,以毫秒为单位,这个方法的返回值表示当前计算机时间和GMT时间(格林威治时间)

调用该方法获取到的时间戳的值可以用在各种场景下,例如:

  • 和UUID一样需要生成随机的东西的业务需求
  • 记录日志或调试:在代码的某个特定点获取时间戳,可以用于记录代码执行的时间,或者在调试中查看程序运行的状态
  • 时间比较:通过比较两个时间戳,可以判断两个事件发生的先后顺序,或者计算两个事件之间的时间间隔
  • 时间计算:时间戳可以作为计算时间长度的基础,例如计算两个日期之间的天数、小时数、分钟数等
  • 时间触发:在一些需要定时触发或者按照时间周期执行的情况下,时间戳可以作为触发条件或者计时器的基础

暑假补课

面试常问问题

java数据类型有哪些以及分别占的字节是多少?
分为基本数据类型和引用数据类型
比特位是计算机存储设备的最小信息单元,操作系统分配内存最少一个1个字节
1字节(byte)=8比特(bit)

在这里插入图片描述

各类型的默认值:
在这里插入图片描述

自动装箱(boxing): 基本数据类型——>包装器类型,例如int型转为integer型
自动拆箱(unboxing): 包装器类型——>基本数据类型
在这里插入图片描述

各类型的默认值作用是占位置占内存

面试题10.0/2
在Java中,当进行除法运算时,如果参与运算的两个操作数都是整数类型,那么结果也会是整数类型。这意味着小数部分会被舍弃,只保留整数部分。但是,如果其中一个操作数是浮点数类型(如10.0),那么结果也会是浮点数类型。

因此,10.0/2的结果是5.0,因为其中一个操作数是浮点数10.0,所以结果也是浮点数类型,并保留了小数部分。

JDK1.8中,HashMap采用数组+链表+红黑树(一种平衡搜索二叉树)实现,当链表长度超过阈值(8)时,将链表转换为红黑树

移位符>>或<<
5>>2就是1——0401

字符串常量池问题

  1. 字符串常量池(堆)
  2. class常量池
  3. 运行时常量池
JVM虚拟机

java之所以是跨平台的,是因为在不同的平台上有不同的虚拟机,java文件通过虚拟机执行,虚拟机再与操作系统交互,操作系统再与硬件打交道。

JVM组成: 类加载器、运行时数据区、执行引擎、本地接口

在这里插入图片描述
在这里插入图片描述
方法执行的时候会创建栈帧并入栈,方法结束则弹栈(出栈)
在这里插入图片描述

类加载器

负责将类的字节码加载到内存中,并将其转换为可执行的Java对象

类加载器的工作原理/过程:
在这里插入图片描述
可简化为:

  1. 加载:根据类的全限定名(包括包路径和类名),定位并读取类文件的字节码
  2. 验证:验证字节码的正确性和安全性,确保它符合Java虚拟机的规范
  3. 准备:为类的静态变量分配内存,并设置默认的初始值
  4. 解析:将类的符号引用(比如方法和字段的引用)解析为直接引用(内存地址)
  5. 初始化:执行类的初始化代码,包括静态变量的赋值和静态块的执行

类加载器采用了双亲委派机制
作用:避免类的重复加载;保证类加载的安全性
在这里插入图片描述

对象的产生过程:
在这里插入图片描述
在这里插入图片描述
字符串常量池:
在这里插入图片描述

equals 和 == 的区别:
  • equals 主要用于判断字符串的值是否相等,它默认是比较地址的但是在被重写后就用来比较值了
  • == 不管是基本数据类型还是引用数据类型,也可以比较值,但主要用于判断指向的地址是否相等

在这里插入图片描述
注意 基本数据类型的比较只能用==而不能用equals

	public static void main(String[] args) {
		//intern方法用于返回字符串常量池的地址,如果字符串常量池中没有则返回堆中的地址
		String str=new String("qwer");	//堆中地址
		String str1=str.intern();	//字符串常量池中地址
		String str2=str.intern();	//字符串常量池中地址
		System.out.println(str==str1);	//false
		System.out.println(str1==str2);	//true
	}
public class Test4 {
	public static void main(String[] args) {
		String str="qwer";//返回常量池地址
		String str2="qw"+"er";//在编译期合并"qwer"
		String str3=new String("qwer");//返回堆中地址
		String str4="qw"+new String("er");//new String("qwer") 返回堆中地址
		String str5="qw";
		String str6=str5+"er";//加法的两边出现了对象的引用,无法在编译期确定其值,因此无法合并
		String str7=new String("er");
		String str8=str5+str7;//new String("qwer") 返回堆中地址
		
		System.out.println(str==str2);//t
		System.out.println(str==str3);//f
		System.out.println(str==str4);//f
		System.out.println(str3==str4);//f
		System.out.println(str==str6);//f
		System.out.println(str==str8);//f
		System.out.println(str3==str8);//f
	}
}
public class Test5 {
	public static void main(String[] args) {
		String str="qwer";
		final String str1="qw";//final修饰的是常量,其值不会再改变
		String str2=str1+"er";//+此时完全可以确定两边的值都是已经不会变的,因此可以合并
		System.out.println(str==str2);
	}
}
public class Test6 {
	public static void main(String[] args) {
		String str1="qwer";
		final String str2=getMsg();//getMsg必须要运行才能返回qw,但这个时候不是编译期,是运行期
		String str3=str2+"er";//+只能在编译期合并
		System.out.println(str1==str3);
	}
	public static String getMsg() {
		return "qw";
	}
}
垃圾回收机制GC

垃圾收集器在对堆进行垃圾回收时,首先要判断哪些对象还活着,哪些对象已死(即不被任何途径引用的对象)

引用: 在JDK1.2之前,引用被定义为当一个reference类型的数据代表的是另外一块内存的起始地址,该类型的数据被称为引用;但对于一些“食之无味,弃之可惜”的对象就显得无能为力,因此在JDK1.2之后引用又分为强引用、软引用、弱引用、虚引用,强度依次递减

  • 强引用(Strong Reference): 是最常见的引用类型,它会在任何情况下都不会被垃圾回收器回收。如果一个对象具有强引用,即使内存不足时,垃圾回收器也不会回收该对象
  • 软引用(Soft Reference): 是一种相对较弱的引用类型。当内存不足时,垃圾回收器可能会回收具有软引用的对象。软引用通常用于实现内存敏感的高速缓存
  • 弱引用(Weak Reference): 是一种更弱的引用类型。当垃圾回收器运行时,无论内存是否充足,具有弱引用的对象都可能被回收。弱引用通常用于实现对象的辅助数据结构,如哈希表
  • 虚引用(Phantom Reference): 是最弱的引用类型。虚引用的存在目的是在对象被垃圾回收器回收时收到一个系统通知。虚引用与其他引用类型不同,它无法通过get()方法获取引用的对象,而只能通过ReferenceQueue来获取相关通知

垃圾回收常用的算法

引用计数器算法(Reference Counting):

  • 该算法通过维护每个对象的引用计数,即记录有多少个指针指向该对象。当引用计数为0时,可以确定该对象已经不再被引用,可以被回收。然而,引用计数器算法无法解决循环引用的问题,因为循环引用的对象的引用计数永远不会为0,即使这些对象已经不再被外部引用。

标记清除算法(Mark and Sweep):

  • 一般和可达性分析算法协同使用,该算法分为两个阶段,标记阶段和清除阶段。首先,从根对象(一般是全局变量、栈上对象等)开始,标记所有与根对象直接或间接相关联的可达对象。然后,在清除阶段,清除未被标记的对象,即被视为垃圾的对象。标记清除算法能够处理循环引用的情况,因为它通过可达性分析来确定哪些对象可以被保留,哪些对象需要被回收。但是有内存碎片问题,而标记整理算法可避免该问题,因为清除后的内存是连续的。

复制算法:

  • 主要用于解决内存碎片化的问题。它将堆内存分为两个相等大小的区域,一半被称为"From"空间,另一半被称为"To"空间,(也可以称为"Eden"区和"Survivor"区)

工作步骤如下:

  1. 初始时,所有活跃对象都位于"From"空间。
  2. 当进行垃圾回收时,垃圾回收器会扫描"From"空间中的所有活跃对象,并将它们复制到"To"空间中,并按照顺序进行排列。
  3. 复制过程中,所有的引用关系也需要更新,指向新的地址。
  4. 完成复制后,"From"空间中的所有对象都被认为是垃圾,可以被回收。
  5. "From"空间和"To"空间的角色发生互换,即"To"空间变为"From"空间,"From"空间变为"To"空间。
  6. 下一次垃圾回收时,将从新的"From"空间开始执行复制过程。

有效地解决内存碎片的问题,避免了内存分配和回收过程中的不连续空间的产生。复制算法可以通过简单的内存拷贝操作来完成,具有较高的效率。但是,复制算法的缺点是需要额外的内存空间用于存储复制的对象,并且在对象存活率较高时,复制的开销可能会比较大。因此,复制算法通常用于新生代的垃圾回收,而不是整个堆内存的回收。

分代算法(现代普遍使用):

分代算法的基本思想是根据对象的生命周期来决定垃圾回收的策略,具体步骤如下:

  • 新生代(Young Generation): 新创建的对象首先被分配到新生代,它是堆内存的一部分。由于大部分对象的生命周期较短,因此新生代采用复制算法进行垃圾回收。新生代又分为 Eden(伊甸园区) 区、Survivor(幸存区)区 From 区 和 Survivor区 To 区(S1和S2)。对象初始分配在 Eden区,当 Eden区满时,会触发MinorGC和停止所有线程的STW(stop the world),Eden区和 Survivor区 From 区中仍然存活的对象会被复制到 Survivor区 To 区,同时做一些年龄判断的操作,当某个对象经过多次复制后仍然存活,则将其晋升到老年代。
  • 老年代(Old Generation): 经过多次垃圾回收后仍然存活的对象会被晋升到老年代。老年代中的对象生命周期较长,因此采用标记-清除或标记-整理算法进行垃圾回收。老年代的垃圾回收称为Major GC(或Full GC),它会对整个堆进行回收。
  • 永久代(Permanent Generation): 永久代存放着一些静态的类信息、方法信息及常量等数据。在一些Java虚拟机中,永久代被废弃,取而代之的是元空间(Metaspace),它的垃圾回收不再是分代算法。

分代算法的核心思想是根据对象的生命周期将堆内存分为不同的区域,并针对不同的区域采取不同的垃圾回收策略。通过这种方式,可以提高垃圾回收的效率和性能。年轻代中的垃圾回收频率较高,而老年代中的垃圾回收频率较低,这样可以减少全堆垃圾回收的次数,提高系统的响应速度。

垃圾收集器

Serial 收集器
Serial 垃圾收集器是一种单线程的串行执行的垃圾收集器,属于 HotSpot 虚拟机的默认选择之一。它主要用于低端设备和客户端应用程序中,对于小型堆空间来说效果较好。

Serial 垃圾收集器的特点如下:

  1. 单线程: Serial 垃圾收集器使用单线程进行垃圾收集工作,即只有一个线程用于执行垃圾回收操作。它通过暂停应用程序的所有用户线程(STW),然后进行垃圾回收。因为只有一个线程在工作,所以不会产生线程同步的开销。

  2. 复制算法: Serial 垃圾收集器主要用于新生代的垃圾回收,采用复制算法进行回收。

  3. 暂停应用: 由于 Serial 垃圾收集器使用单线程进行垃圾回收,它在执行垃圾回收时需要暂停应用程序的所有用户线程。这个暂停时间是可见的,会导致应用程序出现较长的停顿,适用于对响应时间要求不高的场景。

  4. 低延迟: 由于 Serial 垃圾收集器是单线程的,它的垃圾回收工作在后台进行,不会占用过多的系统资源。这使得它适合于低端设备和客户端应用程序,对系统的响应时间没有太大影响。

总结来说,Serial 垃圾收集器是一种简单而高效的垃圾收集器,适用于对于资源受限、对响应时间要求不高的场景。然而它无法充分利用多核 CPU 的优势,并且停顿时间较长,不适合用于服务器端和大型应用程序中。

ParNew 收集器

ParNew 垃圾收集器是 HotSpot 虚拟机中的一种多线程并行的垃圾收集器,它主要用于新生代的垃圾回收,是 Serial 垃圾收集器的多线程版本

ParNew 垃圾收集器的特点如下:

  1. 多线程并行: ParNew 垃圾收集器使用多个线程并行执行垃圾回收操作,可以充分利用多核 CPU 的优势来提高垃圾回收的效率。它在执行垃圾回收时,与应用程序的用户线程并发执行,减少了垃圾回收对应用程序的停顿时间。

  2. 复制算法: ParNew 垃圾收集器同样采用复制算法来进行新生代的垃圾回收。

  3. 与 CMS 收集器配合使用: ParNew 垃圾收集器通常与 CMS (Concurrent Mark Sweep) 收集器配合使用,共同完成整个堆内存的垃圾回收。ParNew 收集器负责新生代的垃圾回收,而 CMS 收集器负责老年代的垃圾回收,两者可以并发执行,减少整体垃圾回收对应用程序的影响。

  4. 适用于多核服务器: 由于 ParNew 垃圾收集器采用多线程并行执行,能够充分利用多核 CPU 的优势,因此非常适合用于多核服务器上的大型应用程序。它可以在减少停顿时间的同时提高垃圾回收的效率。

需要注意的是,ParNew 垃圾收集器仅适用于新生代的垃圾回收,老年代的垃圾回收仍然需要使用其他的收集器。而且由于是并行执行的垃圾收集器,它的停顿时间可能会比 Serial 收集器稍长,但总体上仍然比较低延迟。同时,ParNew 垃圾收集器不支持压缩算法(标记-整理算法、复制算法、分代算法),因此无法进行空间整理。

Parallel Scavenge收集器

与ParNew 垃圾收集器不同的是,Parallel Scavenge的设计目标是达到一个可控制的吞吐量,即最大化程序的运行时间,并且减少垃圾回收的停顿时间,其他都和ParNew差不多。总的来说,它通过并行处理和自适应调节等技术手段,可以提高垃圾回收的效率和性能,达到较高的吞吐量。适用于对吞吐量要求较高的应用程序场景。

CMS收集器

CMS(Concurrent Mark Sweep)是一种并发垃圾收集器,用于进行老年代的垃圾回收。与其他垃圾收集器不同,CMS的设计目标是减少垃圾回收的停顿时间,以提高应用程序的响应性能。

CMS的主要特点如下:

  1. 并发标记: CMS使用并发标记算法进行垃圾回收。它在应用程序运行的同时,使用多个线程对堆内存中的对象进行标记。这样可以减少垃圾回收的停顿时间,提高应用程序的响应性能。

  2. 并发清除: 它在标记阶段完成后,并发地清除垃圾对象。

  3. 分代收集: CMS将堆内存分为年轻代和老年代两个部分。年轻代使用复制算法进行垃圾回收,而老年代使用标记-清除算法进行垃圾回收。通过分代的方式,可以根据对象的特性选择合适的垃圾回收算法,提高垃圾回收的效率。

  4. 低停顿时间: CMS的设计目标是减少垃圾回收的停顿时间,以提高应用程序的响应性能。它通过并发标记和并发清除等技术手段,可以在应用程序运行的同时进行垃圾回收,减少停顿时间。

  5. 内存碎片化: 由于CMS使用标记-清除算法进行垃圾回收,可能会导致内存碎片化问题。为了解决这个问题,CMS提供了一种叫做空闲列表(Free List) 的数据结构,用于管理内存碎片,以提高内存的利用率。

需要注意的是,CMS垃圾收集器在提供低停顿时间的同时,可能会牺牲一定的吞吐量。它适用于那些对于响应时间要求较高、对于停顿时间敏感的应用场景,如Web服务器、交易系统等。对于具有大量数据处理需求、更关注吞吐量的应用场景,可能需要考虑其他类型的垃圾收集器,如Parallel Scavenge或G1等。

G1收集器(目前效果最好)

G1(Garbage-First)是一种面向服务端应用程序的垃圾收集器,G1垃圾收集器在整体设计上与CMS垃圾收集器有相似之处,但它具有更高的吞吐量和更低的停顿时间

G1的设计原则就是简单可行的性能调优
性能调优时的主要参数:XX:+UseG1GC、-Xmx32g、 -XX:MaxGCPauseMillis=200
其中-XX:+UseG1GC为开启G1垃圾收集器,-Xmx32g设计堆内存的最大内存为32G,-XX:MaxGCPauseMillis-=200设置GC的最大暂停时间为2Q0ms。如果我们需要调优,在内存大小一定的情况下,我们只需要修改最大暂停时间即可

G1垃圾收集器的主要特点如下:

  1. 区域化内存管理:G1将整个堆内存划分为多个大小相等的区域(Region),每个区域可以是年轻代或老年代。这种区域化的内存管理方式使得G1可以更加灵活地进行垃圾回收,以满足不同应用程序的需求。

  2. 并发标记:G1使用并发标记算法进行垃圾回收。它在应用程序运行的同时,使用多个线程对堆内存中的对象进行标记。这样可以减少垃圾回收的停顿时间,提高应用程序的响应性能。

  3. 区域化的垃圾回收:G1使用增量式的、区域化的垃圾回收算法。它将堆内存划分为多个区域,并根据垃圾回收的情况动态地选择需要回收的区域。这样可以将垃圾回收的工作分摊到多个时间片段中,减少每次垃圾回收的停顿时间。

  4. 智能回收:G1垃圾收集器具有智能回收的能力。它可以根据应用程序的负载情况和垃圾回收的效果,动态地调整垃圾回收的参数,以达到最佳的性能和吞吐量。

  5. 低停顿时间:G1的设计目标是减少垃圾回收的停顿时间,以提高应用程序的响应性能。它通过并发标记和区域化的垃圾回收等技术手段,可以在应用程序运行的同时进行垃圾回收,减少停顿时间。

JVM相关参数:
在这里插入图片描述

JVM调优

要再花时间学习JVM调优!!!

JVM调优主要是通过调整Java虚拟机中的一些参数来优化Java应用程序的性能。常见调优方法:

  1. 调整堆内存大小:通过-Xms和-Xmx参数来指定Java堆的初始大小和最大大小
  2. 选择合适的垃圾回收器提高垃圾回收的效率和性能,如CMS、G1等
  3. 调整垃圾回收参数:针对所选的垃圾回收器,调整相关参数来优化垃圾回收的性能。例如对于G1垃圾回收器,可以调整参数来控制垃圾回收暂停时间的最小值
  4. 启用JIT编译器优化代码
  5. 对于使用线程的应用程序,可以调整线程池的大小来优化性能。根据程序的特点和服务器配置,可以合理地增加或减少线程数
  6. 启用压缩指针减少内存占用
  7. 关闭不需要的扩展机制
  8. 尽量避免内存溢出和栈溢出,使用工具(如VisualVM、JProfiler等)对JVM进行监控和分析,对于内存溢出可以检查代码中是否存在大对象、频繁创建对象等情况;对于栈溢出可以减少递归深度
事务

事务必需满足ACID(原子性、一致性、隔离性和持久性)特性,缺一不可: 面试经常问

  • 原子性(Atomicity)
    即事务是不可分割的最小工作单元,事务内的操作要么全做,要么全不做
  • 一致性(Consistency)
    在事务执行前数据库的数据处于正确的状态,而事务执行完成后数据库的数据还是处于正确的状态,即数据完整性约束没有被破坏;如银行转帐,A转帐给B,必须保证A的钱一定转给B,一定不会出现A的钱转了但B没收到,否则数据库的数据就处于不一致(不正确)的状态
  • 隔离性(Isolation)
    并发事务执行之间无影响,在一个事务内部的操作对其他事务是不产生影响的,这需要事务隔离级别来指定隔离性
  • 持久性(Durability)
    事务一旦执行成功,它对数据库的数据的改变必须是永久的,不会因比如遇到系统故障或断电造成数据不一致或丢失

在实际开发中数据库操作一般都是并发执行的,即有多个事务并发执行,并发执行常见问题如下:

  • 丢失更新
    两个事务同时更新一行数据,最后一个事务的更新会覆盖掉第一个事务的更新,从而导致第一个事务更新的数据丢失,这是由于没有加锁造成的
  • 脏读
    一个事务看到了另一个事务未提交的更新数据
  • 不可重复读
    在同一事务中,多次读取同一数据却返回不同的结果;也就是有其他事务更改了这些数据
  • 幻读
    一个事务在执行过程中读取到了另一个事务已提交的插入数据,即在第一个事务开始时读取到一批数据,但此后另一个事务又插入了新数据并提交,此时第一个事务又读取这批数据但发现多了一条,即好像发生幻觉一样
SpringMVC八大原理流程

(面试经常问,一定要背熟再去理解为自己的话语,特别重要!!)

  1. 用户向服务器发送请求,请求被Spring前端控制ServeltDispatcherServlet捕获

  2. DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI)。然后根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain对象的形式返回

  3. DispatcherServlet根据获得的Handler,选择一个合适的HandlerAdapter。执行相应的Controller(附注:如果成功获得HandlerAdapter后,此时将开始执行拦截器的preHandler(…)方法)

  4. 提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)。在填充Handler的入参过程中,根据配置,Spring将帮你做一些额外的工作:HttpMessageConveter

    • 将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定的响应信息;
    • 数据转换:对请求消息进行数据转换。如String转换成Integer、Double等;
    • 数据格式化:对请求消息进行数据格式化。如将字符串转换成格式化数字或格式化日期等;
    • 数据验证:验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中

  5. Handler执行完成后,向DispatcherServlet返回一个ModelAndView对象

  6. 根据返回的ModelAndView,选择一个适合的ViewResolver(必须是已经注册到Spring容器中的ViewResolver)返回给DispatcherServlet

  7. ViewResolver结合Model和View,来渲染视图

  8. 将渲染结果返回给客户端

总结概括为: 所有的请求交给dispatcherServlet,将请求解析通过HandlerMapping寻找对应的Handler,再次找到handler对应的HandlerAdapter(适配器),再通过参数的获取(argumentResolvers)以及转换交给对应的handler方法,紧接着执行,将得到的结果ModelAndView交给dispatcherServlet控制的viewResovler视图渲染器进行渲染,最后返回给用户。

图解:
![在这里插入图片描述](https://img-blog.csdnimg.cn/28d70dfddba54b7a83ece14f0e1b0b36.png

乐观锁和悲观锁

都是用来解决并发问题 面试会问

  • 乐观锁(Optimistic Locking): 认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,也就是不采用数据库自身的锁机制,而是通过程序来实现。在程序上,我们可以采用版本号机制或者时间戳机制实现。读取的时候带版本号,写入的时候带着这个版本号,如果不一致就失败,乐观锁适用于多读的应用类型,因为写多的时候会经常失败。在数据库中添加version字段(列)
  • 悲观锁(Pessimistic Locking): 也是一种思想,对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制来实现,从而保证数据操作的排它性。就是独占锁,不管读写都上锁了。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。在SQL语句后使用for update进行加锁锁定,更新完之后释放
redis击穿、穿透和雪崩的原理及解决办法
持久化状态

面试会提问
Hibernate 把对象分为 4 种状态:

  • 持久化状态:当执行session的查询、修改或创建操作时,对象进入持久化状态
  • 临时状态:刚new出来的对象,和hibernate无关,此时不处于 Session 的缓存中,在数据库中没有对应的记录
  • 游离状态 :游离态代表当前对象已经不在session内存中,不被session引用,将被垃圾回收;清除缓存再次查询需要重新访问数据库
  • 删除状态:执行session.delete方法后进入删除状态,持久化对象从缓存中移除,但在执行提交前,数据库仍保存记录

session 的特定方法能使对象从一个状态转换到另一个状态

4 种状态流程图:
在这里插入图片描述

注意以上的几种状态只是Hibernate认为的状态,与java无关,java一般只有两种状态:新建new和垃圾回收GC

持久化状态的快照功能演示:只有访问数据库后才建立快照

UserDetail pd = new UserDetail();
pd.setId(400011);
pd.setName("张三2");
//update将当前对象加入session缓存,进入持久态,但由于没有访问数据库,因此不具备快照功能。
session.update(pd);
//已经在缓存中,不执行查询
UserDetail pd1 = (UserDetail) session.get(UserDetail.class, 400011);
System.out.println(pd1.getName());
//由于没有快照,因此提交时默认将会执行update语句
ts.commit();

进制转换

1 1 1 1
8 4 2 1

二进制——>十进制: 从最低位(右边)开始,分别提取每个位数*2^(位数-1)再相加求和

八转十:位数*8^(位数-1)再相加求和

十六转十:位数*16^(位数-1)再相加求和

0B 1101 1010 0001 = 3489

二进制——>八进制: 从最低位(右边)开始,每三位为一组,不够用0补,换算为八进制数再拼接
0B 1101 1010 0001 = 110 110 100 001 = 6641

二进制——>十六进制: 从最低位(右边)开始,每四位为一组,不够用0补,换算为十六进制数再拼接,10(A)——>15(F)
0B 1101 1010 0001 = 0X DA1

十进制——>二进制: 不断除二取余,直到商为零,从下到上的余数就是二进制
十进制——>八进制: 不断除八取余,直到商为零,从下到上的余数就是八进制
十进制——>十六进制: 不断除十六取余,直到商为零,从下到上的余数就是十六进制

位运算

原码、反码、补码
规则:

  1. 最高位符号位0正1负
  2. 正数的原码、反码、补码都一样(三码合一)
  3. 负数的反码=它原码的符号位不变,其他位取反
  4. 负数的补码=它的反码+1, 反而负数的反码=它的补码-1
  5. 0的反码、补码都是0

补充:

  • java中的数都是有符号的
  • 计算机运算时都是以补码的的方法运算的
  • 看运算结果要看它的的原码

位运算符
规则:

  • 按位与 &:一假则假
  • 按位或 |:一真则真
  • 按位异或 ^:相同为0,不同为1
  • 按位取反 ~:0—>1,1—>0
  • 注意都要先转为补码再进行与或非运算

<<(左移)运算符:将一个数的所有二进制位向左移动指定的位数,右边空出的位用0填充。移动n位相当于将数乘以2的n次方

例如:

int a = 5; // 二进制表示为:0000 0101
int b = a << 2; // 将a左移2位
// b的二进制表示为:0001 0100,转换为十进制为20
System.out.println(b); // 输出:20

“>>”(右移)运算符:将一个数的所有二进制位向右移动指定的位数,左边空出的位用原来的最高位填充。移动n位相当于将数除以2的n次方并向下取整

例如:

int a = 20; // 二进制表示为:0001 0100
int b = a >> 2; // 将a右移2位
// b的二进制表示为:0000 0101,转换为十进制为5
System.out.println(b); // 输出:5

“>>>”(无符号右移)运算符:将一个数的所有二进制位向右移动指定的位数,左边空出的位用0填充。不论原来的最高位是0还是1,都用0填充

例如:

int a = -20; // 二进制表示为:1111 1111 1111 1111 1111 1111 1110 1100
int b = a >>> 2; // 将a无符号右移2位
// b的二进制表示为:0011 1111 1111 1111 1111 1111 1111 1011,转换为十进制为1073741827
System.out.println(b); // 输出:1073741827

需要注意的是,<<、>>和>>>运算符只能用于整数类型(byte、short、int和long),不能用于浮点数类型。

例子:将-10转换为二进制补码

  1. -10的绝对值是10,将其转换为二进制数为 0000 1010。

  2. 反转二进制数的每一位得到 1111 0101。

  3. 对反转后的二进制数进行加1操作,得到最终结果 1111 0110。这就是十进制数-10的二进制补码表示。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

永不掉发的陳不錯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值