静态、抽象、内部类
1.static
static修饰符可以修饰属性、方法、代码块
1.1静态属性(全局变量)
在类中,使用static修饰的属性,就是静态属性。例如,
public class Demo{
static int num; //可共享,即使全局变量
}
注意,非静态属性,是属于对象的(实例变量),一定要使用对象来访问,没有其他方式!
静态属性,是属于类的,并且是这个类所有对象共享的,例如
静态属性在类加载的时候就存在了,即在new对象之前就存在;
可以通过类调用 Demo.num
;
package com.company.static_abstro;
public class Demo {
static int num; //静态属性,全局变量
public static void main(String[] args) {
Demo.num = 10;
Demo demo1 = new Demo();
Demo demo2 = new Demo();
System.out.println(demo1.num);//输出结果为 10
System.out.println(demo2.num);//输出结果为 10
Demo.num = 20;
System.out.println(demo1.num);//输出结果为 20
System.out.println(demo2.num);//输出结果为 20
demo1.num = 30;
System.out.println(demo1.num);//输出结果为 30
System.out.println(demo2.num);//输出结果为 30
}
}
/*
public class Demo{
int num; //必须要new对象才可以使用
public static void main(String[] args){
Demo demo=new Demo();
// demo.num=6;
System.out.println(demo.num);//创建对象时默认初始化=0
}
}
*/
可以看出,无论是使用类访问静态属性,还是使用这个类的某个对象访问静态属性,效果是一样 的,这个属性对这个类的所有对象都是可见的、共享的。
静态属性的存储位置:
类中的静态属性,跟随着类,一起保存到内存的方法区,当创建对象的时候,对象中只会保存类中定义的非静态属性的信息,而静态属性是不会进入到对象中的。
静态属性的初始化:
无论是静态属性还是非静态属性,都必须初始化后才能使用,要么是系统给初始值赋默认值,要么是我们手动给属性赋值;
属性的初始化时间:
- 非静态属性:创建对象后,系统会自动给对象中的非静态属性做默认初始化赋值,也是因为这个原因,非静态属性只有在创建对象后,使用对象采用可以访问
- 静态属性:类加载到内存中(方法区)的时候,系统就会给类中的静态属性做**初始化默认赋值,**所以即使还没有创建对象,只要类加载到了内存中,就可以直接用类名来访文静态属性,因为这个时候静态属性已经完成了初始化默认赋值的操作;
所以,静态属性是属于类的,只要加载到了内存了,就可以使用类名来访问。非静态属性是属于对象的,必须通过创建对象,使用对象才可以访问。
内存图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mqbttxB5-1638149270654)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210609153312668.png)]
- Demo类,被加载到内存中的时候,静态属性num就已经完成了默认初始化赋值操作;
- 可以通过类名(Demo.num)来访问,它可以直接找到方法区中存储的静态变量num;
- 可以通过对象(demo.num)来访问,引用demo先找到堆区中的对象,再根据对象中存储的Demo.class信息,找到方法区中存储的静态变量num;
- 通过上述可知,无论是通过类名还是对象来访问静态变量num,访问的都是同一个num,但是官方推荐用类名来访问更加合适;
注意,只有实例变量才会保存在对象中,并作初始化操作,静态变量保存在类中,并作初始化操作;
生活中的例子:
非静态属性(实例变量),就像教室中,同学们的水杯,每个同学都有一个自己的水杯,和其他人相互 不影响
静态属性,就像教室中,角落里的饮水机,它是这个教室中所有同学共享的,张三接一杯水,李四就会 看到饮水机中的水少了一些,同样的李四接一杯水,张三也会看到饮水机中的水少了一些。
public class Student{
public static int count;
public Student(){
count++;
}
}
public static void main(String[] args){
Student s1 = new Student();
Student s2 = new Student();
Student s3 = new Student();
Student s4 = new Student();
System.out.println(Student.count);//输出 4
}
创建对象一定会调用构造器,只要在构造器中,让变量count累加就行,因为count是一个静态变量,每个Student类型对象共享的,所以每次创建Student对象,使用到的count变量都是同一个
三种变量的对比:
全局变量:
public class Demo{
static int num; //可共享,即使全局变量,不用创建对象就可以用,和对象没有关系,是属于类的
//也可以通过调用对象来访问(完全没必要)
}
局部变量:
public class Demo{
public void f(){
int num; //方法中的定义的变量
}//在这个大括号之前该变量都可以用
}
实例变量:
public class Demo{
int num; //必须要new对象才可以使用
public static void main(String[] args){
Demo demo=new Demo();
// demo.num=6;
System.out.println(demo.num);//创建对象时默认初始化=0
}
}
1.2静态方法
静态方法不能被重写
在类中,使用static修饰的方法,就是静态方法。例如,
public class Demo{
public static void test(){
}
}
静态方法的调用:
可以使用类名来调用,也可以使用对象来调用,但推荐使用类名:
public class Demo{
public static void test(){
}
}
public static void main(String[] args){
Demo.test();//推荐的方式
Demo demo = new Demo();
demo.test();//可以调用,但是不推荐
}
静态方法中不能调用类中的非静态方法或非静态属性:
public class Demo{
public String num;
public static void test(){
this.num = 10;//编译报错
this.sayHello();//编译报错
}
public void sayHello(){}
}
注意,静态方法中,不能访问this,所以也不能访问类中的非静态属性和非静态方法
原理:
在类加载的时候(.java—>.class)加载到内存中,JVM会优先给类中的静态属性做初始化,给类中的静态方法分配内存空间;
而类中的非静态属性的初始化,非静态方法的分配空间,是要等创建对象之后才会进行;
所以类加载好之后,就可以直接使用类名访问静态属性和静态方法了;
创建好对象之后,才可以使用对象来访问非静态属性和非静态方法;
正是因为这个原因,在静态方法中,不能调用类中的非静态属性和非静态方法;
反之,在非静态方法中可是可以访问静态属性和静态方法的,因对象创建在类加载之后;
思考,是否可以在构造器中对类中静态属性做初始化?
public class Demo {
public static int num;
public Demo(){
num = 10;
}
}
这个代码编译是通过的,但是运行的时候可能会存在问题,例如
package com.company.static_abstro;
public class Demo {
public static int num;
public Demo() {
num = 10;
}
public static void main(String[] args) {
System.out.println(Demo.num);//输出结果为 0
Demo demo =new Demo();//创建对象调用了构造器
System.out.println( demo.num);//输出结果为10
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PgPMiYWP-1638149270657)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210609190030363.png)]
可以看出,虽然在构造器中给num赋值为10,但是在访问num的时候,看到的结果却是0
这是因为,构造器是创建对象是才调用的,这里我们直接用类名来访问了num属性,并没有使用到构造器,所以输出的还是默认初始化的值0;
类中的构造器,可以给非静态属性做初始化,但是不能给静态属性做初始化。因为我们可以绕过创建对 象的步骤,直接使用类名访问这个静态属性。
1.3静态代码块
静态代码块,也叫做静态初始化代码块,它的作用就是给类中静态属性做初始化的
例如
public class Demo {
public static int num;
static{
num = 10;
}
}
public static void main(String[] args){
System.out.println(Demo.num);//输出结果为 10
}
在这个情况下,使用类名访问这个静态变量num,输出的结果就是10了。
静态代码块的执行时刻:
由于静态代码块没有名字,我们并不能主动调用,他会在类加载的时候自动执行;
所以静态代码块,可以更早的给类中的静态属性,进行初始化赋值操作。
并且,静态代码块只会自动被执行一次,因为在JVM在一次运行中,对一个类只会加载一次;
匿名代码块:
和静态代码块类似的,还有一种非静态代码块,也叫做匿名代码块,他的作用是给非静态属性做初始化操作;
public class Demo {
public int num;
{
num = 10;
}
}
public static void main(String[] args){
Demo demo = new Demo();
System.out.println(demo.num);//输出结果为 10
}
注意,类中的构造器,既可以给非静态属性进行初始化,也可以给配合new关键字创建对象,所以匿名块使用的场景很少,他能完成的工作,使用构造器也一样可以完成;
匿名代码块执行的时刻:
由于匿名代码块没有名字,所以我们不能主动调用,他会在创建对象的时候,构造器之前,自动执行。
并且每次创建对象之前,匿名代码块都会被自动执行。
例如
public class Demo {
static {
System.out.println("静态代码块执行");
}
{
System.out.println("匿名代码块执行");
}
public Demo(){
System.out.println("构造器执行");
}
}
public static void main(String[] args){
new Demo();
new Demo();
}
//输出结果为:
静态代码块执行
匿名代码块执行
构造器执行
匿名代码块执行
构造器执行
可以看出,静态代码执行了一次,因为JVM只会加载Demo类一次,而匿名代码块会在每次创建对 象的时候,先执行,然后再执行构造器。
1.4创建和初始化对象的过程:
Student s = new Student();
以这句代码为例进行说明:
- 对Student类进行类加载,同时初始化类中静态的属性赋默认值,给静态方法分配内存空间
- 执行父类的静态代码块(若有父类)
- 执行类中的静态代码块
- 堆区中分配对象的内存空间,同时初始化对象中的非静态的属性赋默认值
- 调用Student的父类构造器
- 对Student中的属性进行显示赋值,例如 public int age = 20;
- 执行匿名代码块
- 执行构造器代码
- =号赋值操作,把对象的内存地址赋给变量s
例如
package com.company;
public class Person{
private String name = "zs";
public Person() {
System.out.println("Person构造器");//2
print();//直接去调用子类的方法
}
public void print(){
System.out.println("Person print方法: name = "+name);
}
}
class Students extends Person{
private String name = "tom";//4
{
System.out.println("Student匿名代码块,此时name:"+name);//5
}
static{
System.out.println("Student静态代码块");//1
}
public Students(){
System.out.println("Student构造器");//6
}
//方法重写,故直接调用子类的方法
public void print(){
System.out.println("student print方法: name = "+name);//3
}
public static void main(String[] args) {
Students s=new Students();//7 "=号赋值操作,把对象的内存地址赋给变量s"
//s.print();//再次调用方法,此时在4的时候已经完成name="tom"赋值操作
}
}
运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PM6XYNxg-1638149270660)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210609195626142.png)]
注意,
- 子类重写父类的方法,在创建子类对象的过程中,默认调用的一定是子类中重写后的方法;
- 非静态属性的显式赋值,是在父类构造器执行结束之后和子类中的匿名代码执行之前的时候;
- 以上代码中,因为方法的重写,会调用子类中重写后的print方法,同时该方法恰好实在父类构造器执行中调用的,而这个时候子类的name属性还没有赋值,所以输出结果为null;
- 如果此时你们代码块中也输出了name值,那么就会显示tom,因为已经完成了属性赋值;
1.5静态导入
在自己的类中,要使用另一个类中的静态属性和静态方法,那么可以进行静态导入,导入完成后, 可以直接使用这个类中的静态属性和静态方法,而不用在前面加上类名
注意,只有JDK1.5及以上版本,才可以使用静态导入
例如,没有使用静态导入的情况:
public class Demo {
public void test(){
System.out.println(Math.PI);//访问Math类中的静态属性PI,表示圆周率π
System.out.println(Math.random());//访问Math类中的静态方法random(),生成随机数
}
}
例如,使用静态导入的情况:
import static java.lang.Math.PI;
import static java.lang.Math.random;
//import static java.lang.Math.*; 全部导入
public class Demo {
public void test(){
System.out.println(PI);
System.out.println(random());
}
}
2.final
final修饰符,可以用来修饰类、变量、方法
2.1修饰类
用final修饰的类不能被继承,也就是说这个类没有子类
例如
public final class Action{
}
//编译报错
class Go extends Action{
}
思考,我们是否可以让一个类去继承String?
不能,因为String类使用final修饰的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nT3SF481-1638149270663)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210609201515255.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PiWlwcqc-1638149270665)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210609201656702.png)]
2.2修饰方法
用final修饰的方法可以被子类继承,但是不能被子类的重写
例如
public class Person{
public final void print(){}
}
//编译报错
class Student extends Person{
public void print(){
}
}
思考:我们是否可以在子类中重写从父类Object中继承过来的wait方法?
不能
首先看wait方法是用final修饰的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XQS66gdE-1638149270667)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210609202134306.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PJeMT6FX-1638149270669)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210609202223051.png)]
2.3修饰变量
用final修饰的变量就变成了常量,并且它只能被赋值一次,第二次赋值就会报错
例如,final修饰局部变量
public class Person{
public void print(final int a){
//编译报错,不能再次赋值,传参的时候已经赋过了
a = 1;
}
}
例如,
public class Person{
public void print(){
final int a;
a = 1;
//编译报错,不能再次赋值
a = 2;
}
}
例如,final修饰非静态成员变量
public class Person{
private final int a;
}
对于这个final成员变量a,只有一次赋值机会。
JVM不再为其进行默认赋值,我们需要手动在以下对其进行赋值:
- 声明的同时赋值;
- 匿名代码块中赋值;
- 构造器中赋值,此时还有额外要求:类中出现的所有构造器都要赋值,否则报错;
例如,final修饰静态成员变量
public static void main(String[] args) {
final Student s = new Student();
//编译通过,可以修改s指向对象中的属性值
s.setName("tom");
System.out.println(s.getName());
s.setName("zs");
System.out.println(s.getName());
s = new Student();
//编译报错,不能修改引用s指向的内存地址
}
注意,此时final指的是,引用s的指向的对象不能改变,但是可以使用s来操作当前指向的对象的属性和方法
常量
当static 和final同时修饰一个变量,此时jvm在加载的时候不会给该变量默认初始化,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gFQtnyDx-1638149270671)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210610144551842.png)]
所以必须在声明的时候初始化:
private final static int count=10;
;才不会报错;
但是这项显式赋值之后,因为final修饰的变量只能被赋值一次,所以称为常量;
3.abstract
abstract修饰符,可以修饰类、方法
3.1修饰方法
如果abstract修饰方法,那么该方法就是抽象方法
抽象方法的特点:
- 只有方法声明;
- 没有方法实现;
例如
//这就是一个普通方法,既有方法的声明,又有方法的实现
//方法的声明:public void test()
//方法的实现:{}
public void test(){}
//这就是一个只有声明没有实现的方法
public void test();
//这样的方法需要使用abstract修饰符来修饰,说明它是一个抽象方法
public abstract void test();
可以看出,声明方法的时候,加上abstract修饰符,并且去掉方法的大括号,同时结尾加上分号, 那么该方法就是一个抽象方法。
抽象方法必须定义在抽象类中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6His9kS6-1638149270673)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210609205839181.png)]
3.2修饰类
如果abstract修饰类,那么该类就是抽象类。
例如
public abstract class Action{
}
抽象类和非抽象类的区别:
- 抽象类中使用了abstract修饰符,而非抽象类没有;
- 抽象类中可以编写抽象方法,而非抽象类中不能;
- 抽象类中不能进行直接实例化对象的创建(但是可以使用多态),而非抽象类中可以;
抽象类和抽象方法的关系:
- 抽象类中可以没有抽象方法
- 抽象方法必须在抽象类中;
3.3意义
抽象类就是为了被继承
父类中的一个方法,如果被他的子类重写,并且每个子类各自的实现又不相同,那么父类中的这个方法,只有声明还有意义,而他的方法主体就变得没有任何意义;
这个时候,我们可以吧父类中的这个方法定义成抽象方法,只有方法声明,没有方法主体(也就是方法的实现);
甚至在父类中,一些方法根本就没有办法实现
例如,
public class Animal{
public void run(){
//这里应该编写动物奔跑的代码
}
}
在 Animal 类中,编写 run 方法主体的时候,遇到的问题就是:
虽然我们知道动物都会有奔跑的行为,但是不同的动物奔跑的方式是完全不一样的,那么在 Animal 中,我们并不知道当前是什么动物,这个 run 方法中的代码该如何编写呢?
其实,这里的代码无论怎么编写,都是不合适的,毕竟在 Animal 中,我们并不知道要奔跑的动物到底 是哪一种,并且这个 run 方法将来一定是会被子类重写的,既然如此,那么还不如就索性把 run 定义为 抽象方法,只有方法的声明, 没有方法的实现,将来让子类去实现 run 方法好了。
例如,
public class Cat extends Animal{
//子类重写父类中的run方法,这时候就变得非常明确了,就是一只猫走路的方式
public void run(){
System.out.println("我是一只猫,我踮起脚尖,优雅的走着猫步");
}
}
当子类继承父类,父类为抽象类并且有抽象方法时(两个条件);
子类必须声明是抽象类或者调用父类的抽象方法(父类没有抽象方法即不需要);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1BHpARuw-1638149270675)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210609211445238.png)]
注意,从语法上讲,我们依然可以在Animal中对run方法进行实现,随便写点代码好了,反正又不 会被调用,因为run一定是会被子类重写的,但是这样写代码,就没有了设计的意义了。
所以,很多时候我们不能只看代码的语法是否正确,而且要关注这样的代码设计是否有意义!!
既然 Animal
类中的 run
方法被定义为了抽象方法,那么 Animal
类就一定要声明为抽象类
public abstract class Animal {
public abstract void run();
}
class Cat extends Animal{
public void run(){
System.out.println("我是一只猫,我踮起脚尖,优雅的走着猫步");
}
}
思考:既然抽象类不能被实例化创建对象,那么这个类有什么用?
抽象类是用来被子类继承的,子类继承抽象类,并且实现抽象类中的方法。
所以,当我们遇到一个抽象类的时候,第一反应,应该是这个类肯定需要被继承,然后实现里面的抽象方法,或者重写里面的普通方法。
注意,实现父类中的抽象方法和重写父类中的普通方法,只是说法不同,但是他们的语法要求,操作方法是完全一样的,可以直接把时间抽象方法当作重写普通方法;
思考:抽象类不实例化创建对象,那么抽象类中是否有构造器?
有构造器,这个构造器是让子类调用的,子类继承父类,子类创建对象的时候,会先调用父类的构造器;
思考:如果我们要编写一个类,只想让别人继承它并重写指定方法,那么我们该如何设计?
将这个类定义为抽象类即可,同时如果想要求别人继承的时候,一定要重写某个方法的话,只要把这个 方法定义为抽象方法,别人在继承这个类的时候,就一定要会对这个抽象方法进行实现!
思考:子类继承抽象父类,子类是否可以不实现父类中的抽象方法?
可以实现,子类继承抽象父类,子类可以选择实现父类中所有的抽象方法,如果有任何一个抽象方法没 有被子类实现,那么这个子类也要将自己声明为抽象类,那么这个子类也就只能等待,再来一个子类继承自己,去实现剩余没有实现的抽象方法,直到所有抽象方法都被实现为止。
3.4案例
package com.company.static_abstro;
public abstract class Animal{
public abstract void run();
}
class Cat extends Animal{
//子类重写父类中的run方法,这时候就变得非常明确了,就是一只猫走路的方式
public void run(){
System.out.println("我是一只猫,我踮起脚尖,优雅的走着猫步");
}
}
class Dog extends Animal {
//子类重写父类中的run方法,这时候就变得非常明确了,就是一只狗走路的方式
public void run() {
System.out.println("我是一只狗,我迈开抓子,悠悠晃晃的走着");
}
public static void main(String[] args) {
//编译报错,抽象类不能实例化创建对象
//Animal animal = new Animal();
//声明父类的引用,准备使用多态
Animal animal;
//指向子类对象
animal = new Cat();
//调用方法,子类重写,调用到重写后的方法
animal.run();
//指向子类对象
animal = new Dog();
//调用方法,子类重写,调用到重写后的方法
animal.run();
}
}
4.interface
引用数据类型:类、数组、接口
4.1概述
接口是除了类和数组之外,另外一种引用数据类型
接口和类不同,类的内部封装了成员变量、构造方法和成员方法,而接口的内部主要就是封装了方法和 静态常量。
接口的定义和类很类似,但是接口需要使用 interface
关键字来定义
接口最终也会被编译成.class文件,但一定要明确接口并不是类,而是另外一种引用数据类型
例如,
//使用interface关键字来定义接口
public interface Action {
//接口中的静态常量
public static final String OPS_MODE = "auto";
//接口中的抽象方法
public void start();
//接口的抽象方法
public void stop();
}
注意1, 定义类使用关键字 class
,定义接口使用关键字 interface
注意2,接口中的属性都是公共的静态常量
- 注意:常量的名字一般需要全大写
注意3,接口中的方法都是抽象方法
- 注意:JDK8中,还允许在接口中编写静态方法和默认方法
- 注意:JDK9中,还允许在接口中编写私有方法
注意4,接口的中抽象方法,不需要使用 abstract
修饰符,因为接口中的方法默认就是抽象方法
在接口中,可把一些修饰符省去不写:
//使用interface关键字来定义接口
public interface Action {
//接口中的静态常量
String OPS_MODE = "auto";//默认有static和final修饰
//接口中的抽象方法
void start();
//接口的抽象方法
void stop();
}
接口里面的属性,默认就是public static final修饰的
接口中的方法,默认就是public abstract修饰的
所以都可以省略不写
含有静态方法和默认方法的接口:(JDK8)
public interface Action {
public default void run1() {
// 执行语句
}
public static void run2() {
// 执行语句
}
}
JavaAPI中一些接口中定义的静态方法和默认方法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jRNaDtgn-1638149270679)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210610173030293.png)]
可以看出,绿色圆点表示public修饰的方法
上面有字母D的,就是默认方法,有字母S的就是静态方法,有个字母A的就是抽象方法;
含有私有方法的接口:(JDK9)
public interface Action {
private void run() {
// 执行语句
}
}
4.2接口的实现
类和类之间的关系是继承,类和接口之间的关系是实现:一个类只能有一个父类,但是可以有多个接口;
接口和类不同,类可以实例化创建对象,而接口不行,接口只能让其他类来实现他;
一个类实现了一个接口,那么这个类可以说是接口的实现类。类实现一个接口和类继承一个父类的效果类似,格式相仿,只是关键字不同,实现用implement关键字,继承用extends关键字;
一个类实现了一个接口,那么就要实现接口中所有的抽象方法,否则这个类自己就必须声明为抽象类;
例如,实现一个接口,
public interface Action {
void run();
void sayHello();
}
//类实现接口,没有实现接口中的抽象方法,那么这个类就必须声明为抽象类
abstract class Student implements Action{
}
不然会报错
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jPq8atUT-1638149270680)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210610184828803.png)]
public interface Action {
void run();
void sayHello();
}
//类实现接口,没有实现接口中的抽象方法,那么这个类就必须声明为抽象类
class Student1 implements Action{
@Override
public void run(){
}
@Override
public void sayHello() {
}
}
实现全部的抽象方法就可以不定义该类为抽象类
实现多个接口:
public interface Action {
void run();
void sayHello();
}
interface Mark{
void start();
}
//类实现接口,没有实现接口中的抽象方法,那么这个类就必须声明为抽象类
class Student1 implements Action,Mark{
@Override
public void run(){
}
@Override
public void sayHello() {
}
@Override
public void start() {
}
}
一个类实现了多个接口,那么就需要吧这多个接口中的抽象方法全部都实现
JavaAPI中的String
类,也实现了多个接口;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2XkNcky5-1638149270682)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210610185651390.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GPvcFN9z-1638149270683)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210610185701389.png)]
可以看出String类继承Object类的同时,又实现了三个接口;
4.3接口继承
java中,类和类直接按的继承是单继承,也就是一个子类只能有一个父类,接口和接口之间是多继承。
例如,
//实现该接口的类,将具有run功能
public interface Runnable {
void run();
}
//实现该功能的类将有fly的功能
interface Flyable{
void fly();
}
//实现该接口的类,将具有run的功能,fly的功能,以及Action接口独有的doSomething功能
interface Actions extends Runnable,Flyable{
void doSomething();
}
//实现类,实现Action接口,就必须要实现Action及其父接口中的所有抽象方法
class Demo1 implements Actions{
@Override
public void run() {
}
@Override
public void fly() {
}
@Override
public void doSomething() {
}
}
4.4多态
多态的前提是继承,必须有子父类关系才行,而类和接口之间的实现关系,其实也是继承的一种形式,所以在类和接口的实现关系中,也可以使用多态
接口的引用指向它的实现类对象:
public interface Action {
void run();
}
class Student1 implements Action{
@Override
public void run() {
}
}
class Teacher implements Action{
@Override
public void run() {
}
}
class StudentTest{
public static void main(String[] args) {
//声明接口的引用
Action a;
//可以指向它任意一个实现类对象
a = new Student1();
a = new Teacher();
}
}
接口的引用调用方法,调用到的是实现类中重写的方法
class StudentTest{
public static void main(String[] args) {
Action a = new Student1();
//调用到的方法,是Student中重写(实现)的方法
a.run();
}
}
注意,抽象方法的实现,也是方法重写的一种,形式、语法完全一致;
4.5案例
public interface Action {
void run();
}
interface Mark{
void star();
}
class Student1 implements Action,Mark {
@Override
public void run() {
System.out.println("student run...");
}
@Override
public void star() {
System.out.println("student star...");
}
}
class StudentTest {
public static void main(String[] args) {
Action a = new Student1();
a.run();
a.star();//编译报错
}
}
报错内容:star方法在Action类中不存在
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4jmjlP9I-1638149270686)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210610191413825.png)]
mian
方法中, a.run()
编译运行都是正确的,但是 a.star()
方法的调用是编译错误的。
因为,在编译的时候,编译器会先检查引用 a
所属的类型( Action
)中,是否存在当前要调用的方法 ( star
),如果没有那么就直接编译报错。
正确调用star()方法的代码:
class StudentTest {
public static void main(String[] args) {
Action a = new Student1();
a.run();
// Mark mark=new Student1();//新创建一个对象
Mark m =(Mark)a; //强制转换
// mark.star();
m.star();
}
}
在这里,可以先做类型强制转换,把Action
类型的引用a
,转为Mark类型的引用m
,然后使用引用m
,就 可以调用到Mark接口中的star
方法,同时Student1
中对star
进行了实现(重写),那么最后调用的是 Student1
中的star
方法。
思考,上面代码中,Action类型的引用a,为什么可以转换为Mark类型的引用m?注意,Action和Mark之 间并没有任何关系
Action a = new Student();
Mark m = (Mark)a;
引用a能否转为Mark型,主要是看引用a所指的对象,是否实现了接口Mark,如果是的话,那么就可以转换成功。
同时也可以是instanceof
关键字,对引用a所指向的对象进行判断,是否属于Mark类型,如果是说明该对象实现了Mark接口,属于Mark类型,
例如,
public static void main(String[] args) {
Action a = new Student1();
//返回true,则表示a指向的对象,也同时实现了Mark接口,属于Mark类型,那么就可以做强制转换
if(a instanceof Mark){
System.out.println(a instanceof Mark);//输出true
Mark m = (Mark)a;
}
}
思考,如何让开发人员,一定能按照我们的预先设计好的方法,去编写代码?
用final修饰符修饰方法,即方法不能被访问;
小结:
意义:为了被实现;
implement关键字,就是实现接口==特殊继承;
接口中定义的属性
-
必须初始化或者在匿名块中赋值;
-
默认有static和final修饰的特性;(是常量)
接口中没有构造器;
接口中只能定义抽象方法(不能被具体实现);
接口做到多实现(类似与多继承);
5.访问控制
对象中的属性和方法,是可以根据指定修饰符来进行访问控制的。具体的控制就是,这些属性和方法可以在说明地方被访问,以及在什么地方不能被访问;
5.1概述
类中的属性和方法,可以使用以下四种修饰符进行访问控制:
public >protected >default>private
-
public是公共的,在所有地方都可以访问;
-
protected,受保护的,当前类中,子类中,同一个包中的其他类都可以访问;
-
default,默认的,当前类中,同一个包中的其他类类中可以访问;
- 注意,default默认的,指的是空修饰符,并不是default关键字;
- 例如,String name;在类中,这种情况就是默认的修饰符;
-
private,私有的,只有在当前类中才可以访问;
修饰符 | 同类中 | 同一个包中(子类与非子类类) | 不同包中的子类 | 不同包的非子类 |
---|---|---|---|---|
public | Y | Y | Y | Y |
protected | Y | Y | Y | N |
default | Y | Y | N | N |
private | Y | N | N | N |
5.2案例
package com.company.visit.pkt1;
public class SameCla {
public String public_str = "public_str";
protected String protected_str = "protected_str";
String default_str = "default_str";
private String private_str = "private_str";
//在当前类中访问
public void test(){
System.out.println(public_str);
System.out.println(protected_str);
System.out.println(default_str);
System.out.println(private_str);
}
}
package com.company.visit.pkt1;
import com.company.Day8.Salesman;
public class subclass extends SameCla{
//在同包的子类中访问
public void test(){
//继承后,可直接访问
System.out.println(public_str);
System.out.println(protected_str);
System.out.println(default_str);
//编译报错,无法访问
// System.out.println(private_str);
}
}
package com.company.visit.pkt1;
public class other {
//在同包的其他类中访问
public void test(){
//需要先创建对象,然后再访问
SameCla ac = new SameCla();
System.out.println(ac.public_str);
System.out.println(ac.protected_str);
System.out.println(ac.default_str);
//编译报错,无法访问
System.out.println(ac.private_str);
}
}
package com.company.visit.pkt2;
import com.company.visit.pkt1.SameCla;
public class sub2 extends SameCla {
//在不同包的子类中访问
public void test(){
//继承后,可直接访问
System.out.println(public_str);
System.out.println(protected_str);
//编译报错,无法访问
System.out.println(default_str);
System.out.println(private_str);
}
}
package com.company.visit.pkt2;
import com.company.visit.pkt1.SameCla;
public class deferent_cl {
//在不同包的其他类中访问
public void test(){
//需要先创建对象,然后再访问
SameCla ac = new SameCla();
System.out.println(ac.public_str);
//编译报错,无法访问
System.out.println(ac.protected_str);
System.out.println(ac.default_str);
System.out.println(ac.private_str);
}
}
思考,正常情况下,编写一个类,都可以使用哪些权限控制修饰符?
正常编写的类,可以使用俩种权限控制修饰符:public和default
例如,
public class Person{}
class Student extends Person{}
但是,如果是内部类的话,则可以使用四种权限控制修饰符:
例如,
public class Test{
private class A{}
class B{}
protected class C{}
public class D{}
}
Test类的内部,嵌套类四个内部类:A B C D,分别使用了private 、default、protected、public进行了修饰
6.内部类
内部类,不是在一个java源文件中编写两个平行的类
6.1 成员内部类
在类中,可以定义成员方法、成员变量,除此之外,还可以定义成员内部类
成员内部类内部不能,不能定义静态的内容
例如
//外部类
public class MemberOuterClass{
//外部类的属性
private String name;
private static int age;
//外部类的方法
public void run(){}
public static void go(){}
/* 成员内部类 声明开始 */
public class MemberInnerClass{
private String name;
private int age;
public void run(String name){}
}
/* 成员内部类 声明结束 */
}
注意,成员内部类中,不能编写静态的属性和方法;
注意,当前代码编译成功后。会生成两个class文件,一个对应外部类,一个对应内部类编译生成两个class文件的名字分别为:
MemberOuterClass.class MemberOuterClass$MemberInnerClass.class
成员内部类和外部类的相互访问:
- 成员内部类访问外部的属性和方法
public class MemberOuterClass {
//外部类的属性
private String name;
private static int age;
//外部类的方法
public void run(){}
public static void go(){}
/* 成员内部类 声明开始 */
public class MemberInnerClass{
private String name;
private int age;
public void run(String name){
//访问当前run方法中的参数name
System.out.println(name);
//访问内部类自己的属性name
System.out.println(this.name);
//访问外部类的非静态属性
System.out.println(MemberOuterClass.this.name);
//访问外部类的静态属性
System.out.println(MemberOuterClass.age);
//访问外部类的非静态方法
MemberOuterClass.this.run();
//访问外部类的静态方法
MemberOuterClass.go();
}
}
/* 成员内部类 声明结束 */
}
2.外部类访问成员内部类的属性和方法:
public class MemberOuterClass {
//外部类的方法,访问成员内部类的属性和方法
public void test(){
//需要创建内部类对象,然后才可以访问
MemberInnerClass t = new MemberInnerClass();
System.out.println(t.name);
System.out.println(t.age);
t.run("tom");
}
/* 成员内部类 声明开始 */
public class MemberInnerClass{
private String name;
private int age;
public void run(String name){
}
}
/* 成员内部类 声明结束 */
}
在其他类中使用这个内部类:
如果这个成员内部类不是private修饰的,那么在其他类中就可以访问到这个内部类:
package com.briup.day09.demo;
import com.briup.day09.demo.MemberOuterClass.MemberInnerClass;
public class Test {
public static void main(String[] args) {
MemberOuterClass moc = new MemberOuterClass();
MemberInnerClass mic = moc.new MemberInnerClass();
mic.run("tom");
}
}
在其它类中,使用这个非private修饰的成员内部类的时候要注意以下几点:
- 这个内部类需要import导入,并且是
外部类.内部类
的形式导入;- 在创建对象的时候,需要先创建外部类的对象,然后使用外部类对象再创建内部类对象,形式为:
外部类对象.new 内部类对象
javaAPI中使用的成员内部类:
java.util.ArrayList类中,就定义了好几个成员内部类,并且还是private修饰的[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8JxkvjCg-1638149270688)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210614224009018.png)]
可以,不仅类中可以嵌套接口,接口的内部也可以嵌套其他接口。
例如,参考java.util.Map接口中的内部接口Entry
在对事物进行抽象的时候,若一个事物内部还包含其他事物,就可以考虑使用内部类这种结构。
例如,汽车(Car)中包含发动机(Engine) ,这时, Engine 类就可以考虑(非必须)使用内部类来描 述,定义在Car类中的成员位置。
这样设计,既可以表示Car和Engine的紧密联系的程度,也可以在Engine类中很方便的使用到Car里面的属 性和方法
例如,
public class Car { //外部类
class Engine { //内部类
}
}
例如,人(Person)中包含心脏(Heart),这时,Heart类可以考虑(非必须)使用内部类来描述,定 义在Person类中的成员位置。
例如,
public class Person { //外部类
class Heart { //内部类
}
}
注意,这是从程序中,类和类之间的关系和意义进行考虑而设计的,其实这里即使不使用内部类的结构,使用普通的俩个类也能完成功能,但是内部类的结构设计会加符合实际意义,也能够好的完 成功能,因为内部类访问外部的属性和方法会更加容易。
内部类中使用外部类的实例属性:外部类this.外部类属性名
6.2静态内部类
静态内部类和成员内部类使类似的,只是这个内部类
静态内部类和成员内部类使类似的,只是这个内部类多了个static关键字进行修饰;
例如
//外部类public class StaticOuterClass{ /* 静态内部类 声明开始 */ public static class StaticInnerClass{ } /* 静态内部类 声明结束 */}
注意,静态内部类,可以编写静态的属性和方法,另外在四种内部类中,只有静态内部类可以编写静态属性和方法
编译生成的俩个class文件的名字分别为:
StaticOuterClass.class
StaticOuterClass$StaticInnerClass.class
静态内部类和外部类的相互访问:
1.静态内部类访问外部的属性和方法
public class StaticOuterClass {
//外部类的属性
private String name;
private static int age;
//外部类的方法
public void run(){}
public static void go(){}
/* 静态内部类 声明开始 */
public static class StaticInnerClass{
private String name;
private static int age;
public void run(String name){
//访问当前run方法中的参数name
System.out.println(name);
//访问内部类自己的属性name
System.out.println(this.name);
//访问内部类自己的静态属性age
System.out.println(age);
//静态内部类中,无法访问外部类的非静态属性和方法
//System.out.println(StaticOuterClass.this.name);
//StaticOuterClass.this.run();
//访问外部类的静态属性和方法
System.out.println(StaticOuterClass.age);
StaticOuterClass.go();
}
}
/* 静态内部类 声明结束 */
}
注意,静态内部类中访问不了外部类中的非静态属性和方法;
- 外部类访问静态内部类的属性和方法
public class StaticOuterClass {
public void test(){
//外部类中,访问静态类中的静态属性
System.out.println(StaticInnerClass.age);
//外部类中,访问静态内部类中的非静态属性和方法
StaticInnerClass sic = new StaticInnerClass();
System.out.println(sic.name);
sic.run("tom");
}
/* 静态内部类 声明开始 */
public static class StaticInnerClass{
private String name;
private static int age;
public void run(String name){
}
}
/* 静态内部类 声明结束 */
}
在其他类中使用这个内部类:
如果这个静态内部类不是private修饰的,那么在其他类中就可以访问到这个内部
import com.briup.sync.StaticOuterClass.StaticInnerClass;
public class Test {
public static void main(String[] args) {
StaticInnerClass sic = new StaticInnerClass();
sic.run("tom");
}
}
在其他类中,使用这个非private修饰的静态内部类,需要注意以下几点:
- 这个内部类需要import导入,并且
外部类.内部类
的形式导入;- 在创建对象的时候,直接使用这个静态内部类的名字即可:
new 静态内部类对象();
不再需要依赖外部类对象了
javaAPI中的静态内部类:
java.lang.Integer类中,就定义一个静态内部类,并且还是private修饰的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EEIjMxAN-1638149270690)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210615095425381.png)]
思考,观察Integer类中私有静态内部类IntegerCache,它的作用是什么?
Integer num1 = Integer.valueOf(127);
Integer num2 = Integer.valueOf(127);
System.out.println(num1 == num2); //输出结果为 true
Integer num3 = Integer.valueOf(128);
Integer num4 = Integer.valueOf(128);
System.out.println(num3 == num4);//输出结果为 false
注意,==表示要比较这两个对象的内存地址值是否相等;
6.3局部内部类:(很少用)
局部内部类,是另一种形式的内部,在声明在外部类的方法中,相当于方法中的局部变量的位置,他的作用范围只是在当前方法中。
局部内部类使最不常用的一种内部类。
例如,
public class LocalOuterClass{
public void sayHello(String name){
/* 局部内部类 声明开始 */
class LocalInnerClass{
}
/* 局部内部类 声明结束 */
}
}
局部内部类和外部类的相互访问:
- 局部内部类访问外部的属性和方法
public class LocalOuterClass {
//外部类的属性
private String name;
private static int age;
//外部类的方法
public void run(){}
public static void go(){}
public void sayHello(String name){
/* 局部内部类 声明开始 */
class LocalInnerClass{
private String name;
public void test(String name){
//访问当前test方法中的参数name
System.out.println(name);
//访问内部类自己的属性name
System.out.println(this.name);
/*注意,sayHello方法的参数name,无法访问,因为实在没有办法表示了,换成其他
名字后,就可以访问了,不要叫name就行*/
//访问外部类的非静态属性
System.out.println(LocalOuterClass.this.name);
//访问外部类的非静态方法
LocalOuterClass.this.run();
//访问外部类的静态属性和方法
System.out.println(LocalOuterClass.age);
LocalOuterClass.go();
}
}
/* 局部内部类 声明结束 */
}
}
- 局部内部类中,访问当前方法中的变量,这个变量必须是final修饰的
public void sayHello(final String name){
final int num = 1;
/* 局部内部类 声明开始 */
class LocalInnerClass{
public void test(){
System.out.println(name);
System.out.println(num);
//编译报错,final修饰的变量,只能赋值一次
//name = "tom";
//num = 2;
}
}
/* 局部内部类 声明结束 */
}
在jdk1.8中,一个局部变量在据部内部类进行访问了,那么这个局部变量自动变成final修饰
- 外部类访问局部内部类的属性和方法
public void sayHello(String name){
/* 局部内部类 声明开始 */
class LocalInnerClass{
private int num;
public void test(){
}
}
/* 局部内部类 声明结束 */
//创建局部内部类对象
LocalInnerClass lic = new LocalInnerClass();
//对象访问属性
System.out.println(lic.num);
//对象调用方法
lic.test();
}
局部内部类,只能在当前声明的方法中进行使用。
6.4匿名内部类:(重要)
对于接口就相当于实现接口;
对于类就相当于继承类;
匿名内部类中一般都是会重写接口中的方法或者重写’父类’中的方法;
匿名内部类,是一种没有名字的内部类,他是内部类的一种简化写法。在之后的代码中,匿名内部类是使用最多的一中内部类。
在普通的代码中,使用一个接口的步骤如下:
- 声明一个类,,去实现这个接口;
- 实现这个接口中的抽象方法(重写);
- 在其他代码中,创建这个类的对象;
- 调用这个类中实现(重写)后的方法;
其实,在这个过程中,我们的目的就是吧接口中的抽象方法给实现,最后调用这个已经被实现的方法;
那么,匿名内部类就是把这个过程给简化了,让我们更加方便的调用到重写后的方法
格式:
父类或者接口类型 变量名 = new 父类或者接口(){
// 方法重写
@Override
public void method() {
// 执行语句
}
};
//调用实现(重写)后的方法
变量名.method();
匿名内部类的两种形式:
- 利用一个父类,进行声明并创建匿名内部类对象,这个匿名内部类默认就是这个父类的子类型;
- 利用一个接口,进行声明并创建匿名内部类对象,这个匿名内部类默认就是这个接口的实现类;
匿名内部类因为没有类名:
- 匿名内部类必须依附于一个父类型或者接口
- 匿名内部类在声明的同时,就必须创建对象,否则后面就没法创建;
- 匿名内部类中无法定义构造器;
例如,利用父类型来声明并创建匿名内部类对象;
public abstract class Animal {
public abstract void run();
}
class Test{
public static void main(String[] args) {
Animal animal = new Animal(){
@Override
public void run() {
System.out.println("匿名内部类中的默认实现");
}
};
animal.run();
}
}
注意,如果利用父类型声明这个匿名内部类,那么这个匿名内部类默认就是这个父类型的子类
例如,利用接口来创建匿名内部类对象;
public interface Action {
void run();
}
class Test{
public static void main(String[] args) {
Action a = new Action(){
@Override
public void run() {
System.out.println("匿名内部类中的默认实现");
}
};
a.run();
}
}
注意,如果利用接口声明匿名内部类,那这个匿名内部类默认就是这个接口的实现类;
思考,对比之前普通的方式,匿名内部类是不是在调用到重写方法的同时,简化了之前很多步骤?
//Algorithm 算法接口
public interface Algorithm{
void sort(int[] arr);
}
class Test{
//使用指定算法,对数组arr进行排序
public void sort(int[] arr,Algorithm alg){
alg.sort(arr);
}
}
public static void main(String[] args){
Test t = new Test();
int[] arr = {4,1,6,3,8,5,9};
Algorithm alg = new Algorithm(){
public void sort(int[] arr){
//使用当前需要的排序算法
//例如,这里简单的使用Arrays工具类中的排序方法
java.util.Arrays.sort(arr);
}
};
t.sort(arr,alg);
}
注意:匿名内部类中不能在给外部的属性重新赋值,因为对于内部类来说这里外部的属性默认有final修饰
内部类的选择:
假设现在已经确定了要使用的内部类,那么一般情况下,该如何选择?
- 考虑这个内部类,如果反复的进行多次使用(必须有名字)
- 在这个内部类中,如果需要定义静态的属性和方法,选择使用静态内部类;
- 在这个内部类中,如果需要访问外部类的的非静态属性和方法,选择使用成员内部类
- 考虑这个内部类,如果只需要使用一次(可以没有名字)
- 选择使用匿名内部类
- 局部内部类几乎不会用;
7.包装类
java中的八种基本数据类型,他们只表示一些简单的数字,这些数字最小的在内存中占8位,最大的占64位。这些都是简单的数字,不是对象,所以也不能用来调用方法或者属性。
7.1概述
针对这八种基本类型,JavaAPI又专门提供了对应的类类型,目的就是为了分别把这八种基本类型的数 据,包装成对应的类类型,这时候就变成对象了,就可以调用方法了或者访问属性了。
基本类型 | 包装类型 |
---|---|
byte | java.lang.Byte |
short | java.lang.Short |
int | java.lang.Integer |
long | java.lang.Long |
float | java.lang.Float |
double | java.lang.Double |
boolean | java.lang.Boolean |
char | java.lang.Charcater |
7.2案例
在这些包装类型中,都定义相关的属性和方法,例如Integer中:
package com.company.static_abstro;
public class Integer_test {
public static void main(String[] args) {
int i=1;
//编译报错,因为i不是对象
//i.toString();
// int -->Integer 包装
Integer o = new Integer(i);
Integer o1=Integer.valueOf(i);
System.out.println(o);//输出1
System.out.println(o1);//输出1
//Integer --> int 拆箱
int j=o.intValue();
System.out.println(o.toString());//输出1
System.out.println(Integer.MAX_VALUE);//范围
System.out.println(Integer.MIN_VALUE);
System.out.println(Integer.toString(100,2));//二进制
System.out.println(Integer.toString(100,8));//八进制
System.out.println(Integer.toString(100,16));//十六进制
System.out.println(Integer.toBinaryString(100));//把100转成二进制
//把字符串"100"转为int类型的100
int num = Integer.parseInt("100");
System.out.println(num);
}
}
7.3自动装箱/拆箱
JDK1.5或以上,可以支持基本类型和包装类型之间的自动装箱、自动拆箱:
简化了基本类型和包装类型之间的转换,这里以int和Integer为例说明
//JKD1.5 之前
Integer o = new Integer(1);
Integer o = Integer.valueOf(1);
//JDK1.5 之后
//自动装箱,这里会自动把数字1包装成Integer类型的对象
Integer o = 1;
//JKD1.5 之前
Integer o = new Integer(1);
int i = o.intValue();
//JDK1.5 之后
Integer o = new Integer(1);
//自动拆箱,这里会自动把对象o拆箱为一个int类型的数字,并把数字赋值给int类型的变量i
int i = o;
注意,其他的基本类型和包装类型之间的转换,与此类似
注意事项:
public void test1(int i){}
public void test2(Integer i){}
public void test3(long i){}
public void test4(Long i){}
public static void main(String[] args) {
Integer_test t =new Integer_test();
t.test1(1);//编译通过,int i=1;正常赋值
t.test2(1);//编译通过,Integer i = 1; 自动装箱
t.test3(1);//编译通过, long i = 1; 隐式类型转换
//编译报错
//错误的代码:Long i = 1;
//int和Long 之间没有任何关系
t.test4(1);
t.test4(1L);//编译通过 Long i = 1L; 自动装箱
}
8 .Object中常用方法
Object类是所有类的父类型,类中定义的方法,java中所有对象都可以调用
8.1 toString方法
该方法可以返回一个对象默认的字符串形式:
public class Object{
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
}
子类可以对该方法重写:
public class Student{
private String name;
private int age;
//get
//set
public String toString() {
return "Student[name="+name+",age="+age+"]";
}
}
toString方法的调用:
public static void main(String[] args){
Student stu = new Student();
//这个俩个输出语句的效果是一样的
System.out.println(stu);
System.out.println(stu.toString());
}
注意,默认情况下。println方法会调用这个对象的toString方法;
注意,推荐使用第一种,因为当stu的值为null时,第二种输出方式会报错,空指针异常
8.2 getClass方法
这是一个非常重要的方法,它可以返回一个引用在运行是所指向的对象。具体类型是什么
该方法时native修饰的本地方法,不是java语言实现的。
public class Object{
public final native Class<?> getClass();
}
子类中不能重写getClass方法,调用的一定是Object的中getClass方法:
public void test(Object obj){
//obj可以接收任意类型的对象
//getClass方法可以返回obj指向的对象具体是什么类型的
//也就是该对象到底是使用哪个类创建出的对象
System.out.println(obj.getClass());
}
8.3 equals方法
该方法可以比较两个对象是否相等
public class Object{
public boolean equals(Object obj) {
return (this == obj);
}
}
可以看出来,Object中的equals方法,是直接使用的==号进行的比较,比较两个对象的地址值是否相等
例如,
public static void main(String[] args){
Student o1 = new Student();
Student o2 = new Student();
boolean f = o1.equals(o2);
System.out.println(f);
}
子类中可以对该方法进行重写:
public class Student {
private String name;
private int age;
public Student(){}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
//如果obj为null,直接返回false
if(obj == null){
return false;
}
//如果obj和this的地址值相等,直接返回true
if(this == obj){
return true;
}
//如果obj不属于Student类型的对象,直接返回false
if(!(obj instanceof Student)){
return false;
}
//obj属于Student类型的对象,做类型强制转换
Student other = (Student) obj;
//如果obj的name和age分别等于this的name和age,直接返回true
if(this.name.equals(other.name) && this.age==other.age){
return true;
}
//其他情况,直接返回false
return false;
}
}
class StudentTest{
public static void main(String[] args) {
Student s1 = new Student("tom",20);
Student s2 = new Student("tom",20);
System.out.println(s1 == s2);//输出false,因为俩对象的内存地址不同
System.out.println(s1.equals(s2));//输出true,因为重写了equals的比较规则,name
和age相等就算俩对象相等
}
}
按照重写后的equals比较规则,只要俩个对象的name和age属性值都相等,那么就返回true,说明 俩对象相等
前面的八大包装类型都对equals方法进行了重写
对equals方法的重写,一般需要注意以下几点:
- 自反性:对于任意引用obj,obj.equals(obj)的返回值一定为true;
- 对称性:对于任意引用o1、o2,当且仅当o1.equals(o2)返回值为true时,o2.equals(o1)的返回值一定为true;
- 传递性:如果o1.equals(o2)为true, o2.equals(o3)为true,则o1.equals(o3)也一定为true
- 一致性:如果参与比较的对象没任何改变,则对象比较的结果也不应该有任何改变
- 非空性:任何非空的引用obj,obj.equals(null)的返回值一定为false
8.4 hashCode方法
该方法返回一个int值,该int值是JVM根据对象在内存中的特征(地址值),通过hash算法算出一个结果;
Hash,一般翻译叫做“散列”,也可以音译为:哈希。就是把任意长度的数据输入,通过散列算法,变换成固定长度的输出,该输出就是散列值;
一个任意长度的输入转为一个固定长度的输出,是一种压缩映射,也就是说,散列值的课间通常远远小于输入空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值
所以,我们可以认为,Object中的hashCode方法默认返回的是对象的内存地址,但实际上可能并不是;
public class Object{
public native int hashCode();
}
对于两个对象的hashCode值:
- 相等的两个对象,hashCode值一定相等;
- hashCode值相同,两个对象有可能相同,也有可能不同;
- hashCode值不同,两个对象不一定不同;
对象和它的hashCode值的关系,就相当于人和他们姓氏的关系:
- 相等的两个人,姓氏肯定是一致的;
- 姓氏相同的两个人,不一定是同一个人;
- 姓氏不同的两个人,一定是不同的两个人;
现在有100个对象,突然又来了一个对象obj,那么怎么判断这个obj对象和之前的100个对象是否有相等 的呢?
除了使用obj对象的equals方法和之前的100个对象比较100次之外,是否还有更好的方式?
9 .关于String对象
字符串String,是程序中使用的最多的一种数据,JVM在内存中专门设置了一块区域(字符串常量池),来提高字符串对象的使用率;
9.1 概述
创建字符串对象,和其他普通对象一样,会占用计算机的资源(时间和空间),作为最常用的数据类型,大量频繁的创建字符串对象,会极大程度的影响程序的性能。
JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化
- 为字符串开辟了一个字符串常量池,类似于缓冲区;
- 创建字符串常量时,首先会检查字符串常量池中是否存在该字符串,如果存在,则返回该实例的引用,如果不存在,就实例化创建该字符串,并放入池中;
String s=new String("abc");
上述语句创建了两个对象…其内容都是"abc".注意,s 不是对象,只是引用.只有 new 生成的才是对象.
,首先括号里的"abc"先到 String pool 里看有没"abc"这个对象,没有
则在 pool 里创建这个对象…所以这里就在 pool 创建了一个"abc"对象.然后 通过
new 语句又创建了一个"abc"对象…而这个对象是放在内存的堆里. .这里的 s 指
向堆里的对象.
String s1 = "abc";
,s1 当然还是引用 .后面的"abc".其实就是上面括
号里的"abc".执行的是相同的操作.即 在 pool 里查找有没"abc"这个对象.没有则
创建一个…很显然,第一条语句在 pool 里已经创建了一个"abc".所以这条语句没
有创建对象,s1 指向的是 pool 中的"abc"
String s = new String("abc");
String s1 = "abc";
String s2 = new String("abc");
System.out.println(s == s1.intern());//false
System.out.println(s == s2.intern());//false
System.out.println(s1 == s2.intern());//true
System.out.println(s1.intern() == s2.intern());//true
s1.intern
().他的执行流程是,在 pool 里去查找 s1 对应的内容(也就是"abc").如果
找到,则返回 pool 里的对象.如果没有(老实说,我没想到有哪种情况是没有的),则
在 Pool 创建这个对象,并返回…
这样就很容易理解了.s1.intern 返回的是 pool 里的"abc"对象.与 s 这个堆里的对
象肯定不同,返回 false.同理,s 与 s2.intern()也肯定不同,返回 false.第三个,s1 与
s2.intern().其中 s2.intern()返回的是 pool 中的"abc"对象,而 s1 也是指向 pool 中
的"abc"对象.所以返回的是 true:
9.2 案例
案例1:
public static void main(String[] args) {
//只有用双引号,里面加入文字的方式,才会利用到内存中的字符串常量池。
String str1 = "hello";
String str2 = new String("hello");
//String中对equals进行了重写,比较的是字符串中每一个字符是否相等
System.out.println(str1.equals(str2));//true
//str1指向的是常量池中的hello对象
//str2指向的是堆区中新创建的hello对象
System.out.println(str1 == str2);//false
}
只有用双引号,里面加入文字的方式,才会用到内存中的字符串常量池
对应的内存图为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FFhPikm9-1638149270691)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210615172427638.png)]
案例2:
String str1 = "hello";
String str2 = "hello";
//String中对equals进行了重写,比较的是字符串中每一个字符是否相等
System.out.println(str1.equals(str2));//true
//str1指向的是常量池中的hello对象
//str2指向的是常量池中的hello对象
//str1和str2指向的是同一个对象
System.out.println(str1 == str2);//true
案例3:
String s1 = "a";
String s2="b";
//使用+拼接的字符串也会利用字符串常量池
//但是有要求:参与+号拼接的必须是双引号的形式才可以。
String s3 = "a"+"b";
String s4 = s1+s2;
System.out.println(s3.equals(s4));//true
//s3指向的是常量池中的"ab"对象
//s4指向的堆区中新建的"ab"对象
//因为s4是由s1和s2使用+号连接得到的,而s1和s2都是变量
//有变量参与拼接字符串,那么就不会使用常量池了
System.out.println(s3 == s4);//false
注意,使用"+"拼接字符串也会利用字符串的常量池,但是参与+号拼接的必须时双引号的形式才可以
案例4:
final String s1 = "a";
final String s2 = "b";
//使用+拼接的字符串也会利用字符串常量池
//但是有要求:参与+号拼接的必须是双引号的形式才可以。
String s3 = "a"+"b";
String s4 = s1+s2;
System.out.println(s3.equals(s4));//true
//s3指向的是常量池中的"ab"对象
//s4指向的是常量池中的"ab"对象
//因为s4是由s1和s2使用+号连接得到的,而s1和s2都是final修饰的【常量】
//常量是固定不会变的,在编译期间就能计算出s4的值
//这时候又可是有到字符串常量池了
System.out.println(s3 == s4);//true
注意,final修饰的变量既为常量,常量的拼接在编译期间就可以得出结果,存放到字符串常量池中。
String s1 = "a";
String s2 = "b";
//使用+拼接的字符串也会利用字符串常量池
//但是有要求:参与+号拼接的必须是双引号的形式才可以。
String s3 = "a"+"b";
String s4 = (s1+s2).intern();
System.out.println(s3.equals(s4));//true
//s3指向的是常量池中的"ab"对象
//s4指向的是常量池中的"ab"对象
//intern方法可以在JVM在运行期间,强行使用字符串常量池
//检查当前调用intern方法的字符串,是否在常量池中,如果在那么就返回常量池中的这个对象,如果不在,
那么就把当前这个调用intern方法的字符串存到常量池,供后面的代码使用。
System.out.println(s3 == s4);//true
当调用intern()方法时,JVM会将字符串添加到常量池中,并返回指向该常量的引用
一个单例模式
注意点:
- 构造器私有;
- 饿汉式立即加载对象,可能会造成空间浪费,一般不使用
package com.example.demo.single;
/**
* @Description: 饿汉式单例
* @author: ZYQ
* @date: 2021/3/25 10:53
*/
public class Hungry {
/**
* 浪费空间
*/
/*private byte[] data1 = new byte[1024 * 1024];
private byte[] data2 = new byte[1024 * 1024];
private byte[] data3 = new byte[1024 * 1024];
private byte[] data4 = new byte[1024 * 1024];*/
//保证构造器私有
private Hungry() {
}
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance() {
return HUNGRY;
}
}
饿汉式
DCL懒汉式
注意点:
- 构造器私有;
- 需要使用双重检测,若为单单次检测,则多线程可能同时进入if中,照成多次构造;
- 需要对 lazyMan 对象添加 volatile 修饰词。因此new对象的过程不是原子性的,其分为3步:1.为对象分配内存空间 2.初始化对象 3.将实例指向该空间。在此过程中可能会发生指令重排导致第三步优先于第二步执行。当第三步执行完而还未进行初始化时,若此时有其余线程来到第一层判断,则会误判为lazyMan对象非空,从而直接返回一个空的lazyMan,造成错误。
package com.example.demo.single;
/**
* @Description: 懒汉式单例
* @author: ZYQ
* @date: 2021/3/25 11:05
*/
public class LazyMan {
//构造器私有
private LazyMan() {
System.out.println("构造1次");
}
private static volatile LazyMan lazyMan = null;
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
LazyMan.getInstance();
}).start();
}
}
}
DCL懒汉式