Java学习笔记(二)

本文深入探讨Java类加载机制,详细解析Static关键字的作用,包括静态变量、静态方法和静态代码块的执行顺序。同时,介绍了类的加载过程,main方法的执行原理,以及单例模式的饿汉式和懒汉式的实现。最后,讨论了final关键字的使用,内部类的概念和应用场景,展示了如何在不同场景下有效利用这些特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

类加载

类被加载的三种情况:

  1. 创建实例对象时
  2. 创建子类对象时,父类也会被加载(父类会先被加载)
  3. 使用类的静态成员时

Static:

表示静态,可作用在:

  • 变量
  • 方法
  • 匿名方法块

变量

静态变量为类中所有方法共享,独立存在类中,可被直接调用(不用创建对象,使用 类名.变量名 直接调用 )。是在类加载的时候生成的。
static修饰的变量在内存中只会有一份拷贝。
JDK 8/7 以前,静态变量是类加载的时候放在方法区中。在之后的新版本中,是在堆内存中。

注意,静态变量仍受访问权限修饰符的控制,如果使用 private 修饰,将不能通过类名和对象名进行引用。
静态变量随着类加载就生成,随着类销毁而消失。

public class Potato {
    static int price = 6;
    String content = "";

    public Potato(int price, String content) {
        this.price = price;
        this.content = content;
    }

    public static void main(String[] args) {
        System.out.println(Potato.price);
        System.out.println(">>>>>>>>>>>>>>>>>>>>>>>");
        Potato obj1 = new Potato(10, "酸辣土豆丝");
        System.out.println(Potato.price);
        System.out.println(obj1.price);
        
		Potato obj2 = new Potato(20, "甜辣土豆丝");
        System.out.println(Potato.price);
        System.out.println(obj2.price);
    }
}

输出结果为:

6
>>>>>>>>>>>>>>>>>>>>>>>
10
10
20
20

可以看到,当新建一个对象更改类中的static变量price后,将会直接修改类的price的值。
事实上,在上述例子中的Potato类new的所有的对象会指向同一个static int price。 它是所有对象的共享成员。

方法

静态方法无需通过对象来引用,而通过类名可以直接引用。
工具类的方法一般都为静态方法。方便我们在使用方法时,不用创建实例就可直接使用。

  • 静态方法内只能调用静态内容
  • 静态方法中也不能使用 this 和 super 指针
  • 静态方法不能被重写,可以被重载
  • 非静态的方法可以访问静态内容。不过 非静态对象 访问 静态方法 会报警告
  • static 不可以修饰构造函数

例子:

package courseTest;

public class StaticTest {
    int a = 111;
    static int b = 22222;
    
    public static void hello() {
        System.out.println("0000000");
        System.out.println(b);
        // System.out.println(a); // error, 非静态变量在此不可调用
        // hi(); // error, 非静态方法在此不可调用
    }

    public void hi() { // 非静态方法可以调用静态方法和静态变量
        System.out.println("3333333333");
        hello();
        System.out.println(a);
        System.out.println(b);
    }
    
    public static void main(String[] args) {
        StaticTest.hello();
        // StaticTest.hi(); // error, 非静态方法不可用类名访问
        StaticTest foo = new StaticTest();
        foo.hello();
        foo.hi();

    }
}

类和块

static还可用来修饰类(内部类),但是使用场景比较少。不做介绍。

static块:

  • 只在类第一次被加载(而非实际执行,比如main函数在一个类中,而这个类包含static块,进入main函数时需要加载类,即会执行static块)时调用,即在程序运行期间只会执行一次
  • 执行顺序: static成员 > 匿名(普通)成员 > 构造函数,如果有多个 static/匿名(普通) 内容,按照定义顺序执行
class StaticBlock {
	static { // static块
		System.out.println("222222");
	}
	{ // 匿名块
		System.out.println("111111");
	} 
	public StaticBlock() { // 构造函数
		System.out.println("333333");
	}
	{ // 匿名块
		System.out.println("444444");
	}
}

public class StaticBlockTest {

	public static void main(String[] args) {
		System.out.println("000000");

		StaticBlock obj1 = new StaticBlock();
		StaticBlock obj2 = new StaticBlock();
	}
}

运行结果:

000000
222222
111111
444444
333333
111111
444444
333333

可以看到,第一次new一个StaticBlock对象时,会先执行static块,再执行匿名块,最后执行构造函数。而第二次new时,是先执行匿名块,最后执行构造函数,不再执行static块。之后无论再new多少次,结果都是一样的。
块代码不建议使用。

代码块

就是没有修饰符,也没有返回值和参数以及方法名的方法体。被一堆大括号包起来。
普通代码块会新建一个类的对象的时候调用。

  • 相当于另一种形式的构造器,可以做初始化的操作
  • 比如:当多个构造器有重复语句时,可以抽取到代码块中,提高代码重用性
  • 执行顺序: static成员 > 匿名(普通)成员 > 构造函数,如果有多个 static/匿名(普通) 内容,按照定义顺序执行

构造器的最前面其实隐含了 super() 和 普通代码块

例子:

public class blockTest01 {
    public static void main(String[] args) {
        new BlockB();
    }
}
class BlockA {
    static {
        System.out.println("A的 static 块");
    }
    {
        System.out.println("A的 普通块");
    }
    public BlockA() {
        // 隐含 super(),但是 super 是 Object,无内容
        // 隐含 普通块
        System.out.println("A的构造器");
    }
}
class BlockB extends BlockA{
    static {
        System.out.println("B的 static 块");
    }
    {
        System.out.println("B的 普通块");
    }
    public BlockB() {
        // 隐含 super()
        // 隐含 普通块
        System.out.println("B的构造器");
    }
}

执行结果:

AstaticBstaticA的 普通块
A的构造器
B的 普通块
B的构造器

在上述例子中可以看到,新建一个实例对象的过程是:

  1. 加载对象类的父类
  2. 加载对象类
  3. 调用构造函数(构造函数中先调用了super(),再调用普通块,最后才是构造函数本身的内容)

新建一个子类对象的实现过程

创建一个子类对象时,其与父类的静态代码块、静态属性初始化,普通代码块、普通属性初始化,构造方法的调用顺序如下:

  1. 父类的静态代码块和静态属性(优先级一样,按定义顺序执行)
  2. 子类的静态代码块和静态属性(优先级一样,按定义顺序执行)
  3. 父类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行)
  4. 父类的构造方法
  5. 子类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行)
  6. 子类的构造方法

main 方法

main 方法是我们程序执行时的入口,通过调用 main 方法来执行我们需要的功能。
Java 的 main 方法的方法头部为 public static void main(String[] args),原因:

  1. JVM 编译执行时,并不会和 main 方法在一个包里或者有继承关系,因而需要该方法的访问修饰符是 public
  2. 同样,JVM 执行时,并不会创建一个 main 方法所在类的实例对象再去调用 main,而是直接通过类名调用 main 方法,就像调用工具类的方法一样,所以需要加 static 才可以通过类名调用
  3. void 表示main方法没有返回值,直接调用就可以
  4. 最后 main 方法的形参是一个字符串数组,它会接受在执行时给定的命令行输入内容
    如:
// 编译 java 文件
javac Hello.java
// 一般执行情况
java Hello
// 加参数的情况
java Hello Jerry Is A DOG
/**
* 在上面的情况中,main方法执行时形参args 就会有四个参数了
**/

使用 main 方法时还应该注意:

  • main 方法中可以调用所在类的静态属性和静态方法
  • 不可以调用非静态成员,只能通过创建当前类的实例对象来访问非静态成员

单例模式

又名单态模式。
限定某一个类在整个程序运行过程中,在内存空间只能保留一个实例对象。
保证一个类有且只有一个对象。

  • 采用static共享对象实例
  • 采用private构造函数

饿汉式

饿汉式是指无论当前是否使用单例对象,都先创建单例对象(类加载即创建)。可能会造成资源浪费。

实现方式:

  1. 将构造器私有化
  2. 在类的内部直接 new 一个对象,需要声明访问权限 private,以及 static 类型(因为获取对象的方法是 static 类型)
  3. 提供一个公共的 static 类型方法(这样才可以不创建对象就通过类名访问该方法),返回单例对象

public class Singleton {
    private static Singleton obj = new Singleton();
    private String content;

    private Singleton() {
        this.content = "aba";
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public static Singleton getInstance() {
        // 静态方法使用静态变量
        // 可以再方法内使用临时变量,不可使用非静态变量
        return obj;
    }

    public static void main(String[] args) {
        Singleton obj1 = Singleton.getInstance();
        System.out.println(obj1.getContent());

        Singleton obj2 = Singleton.getInstance();
        System.out.println(obj2.getContent());

        obj2.setContent("def");
        System.out.println(obj1.getContent());
        System.out.println(obj2.getContent());

        System.out.println(obj1 == obj2);
    }
}

运行结果:

aba
aba
def
def
true

在刚刚的例子中,可以看到,外界想使用Singleton类的对象,只能通过getInstance()方法拿到对象obj的指针,而且拿到的对象实例都为obj。

懒汉式

单例对象被调用时才真正创建对象。

实现过程:

  1. 构造器私有化
  2. 定义一个 static 类型对象,但是不创建(new)
  3. 提供一个 public 的 static 类型方法,返回该对象 (当单例对象未创建时,先创建)

代码:

public class Singleton {
	// 定义但不创建
    private static Singleton obj;
    private String content;

    private Singleton() {
        this.content = "aba";
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public static Singleton getInstance() {
        // 当对象未创建时才创建,其他情况直接返回
        if (obj == null)
        	obj = new Singleton();
        return obj;
    }

    public static void main(String[] args) {
        Singleton obj1 = Singleton.getInstance();
        System.out.println(obj1.getContent());

        Singleton obj2 = Singleton.getInstance();
        System.out.println(obj2.getContent());

        obj2.setContent("def");
        System.out.println(obj1.getContent());
        System.out.println(obj2.getContent());

        System.out.println(obj1 == obj2);
    }
}

final:

final可以用来修饰:

  • 方法
  • 变量

final的类不可被继承。

如:

final public class FinalFather {
    
}

class Son1 extends FinalFather {
    // 这里会报错。因为Son1继承了final类型的类
}

包装类 Integer/Double/Float/Boolean 等都是 final 类,String 也是

方法

父类中若有final方法,子类不能改写该方法。
若类已经是 final 修饰的,那么方法一般不会再修饰成 final。
final 不能修饰构造函数

如:

public class FinalMethodFather {
    public final void fi() {

    }
}

public class FinalMethodSon extends FinalMethodFather {
    public void fi() {
        // 这里会报错,因为子类尝试改写父类的final类型的方法
    }
}

变量

final的变量,不能再次赋值。

  • 如果是基本类型,则值不能修改
  • 如果是对象实例,那么不能修改其指针(可以修改对象内部值)

final 修饰的属性,在定义的时候必须赋初值,可以在如下的位置赋初值:

  1. 定义时直接赋值
  2. 在构造器中
  3. 在代码块中

例子

public class FinalPrimitive {

    public static void main(String[] args) {
        final int a = 5;
        a = 10; // 这里会报错,因为尝试修改final类型的值
    }
}
class FinalObject {
    int a = 10;    
}

public class FinalObjectTest {

    public static void main(String[] args) {
        final FinalObject obj1= new FinalObject();
        System.out.println(obj1.a);
        obj1.a = 20;
        System.out.println(obj1.a);

        obj1 = new FinalObject(); // 这里会报错,因为尝试修改final类型对象的指针 
    }
}

final 与 static 搭配使用

编译器做了底层优化,比如

public FinaStatic() {
	public final static int num = 6666;
	staitc {
		System.out.println("静态块执行了");
	}
}

在上面的类中,若 使用类名调用属性 num,将不会完整的加载类,也就是说 static 块不会执行,节省内存。

内部类

一个类的内部又完整的嵌套了另一个类结构。被嵌套的称为内部类,嵌套它的称为外部类。对于和外部类在同一个文件中,但是并未嵌套的类,称为内部类的外部其他类,如

class Outer {	// 外部类
	class Inner {	// 内部类
	}
}

class Other {	// 外部其他类
}

类的五大成员:属性、方法、构造器、代码块、内部类

内部类的最大特点是可以直接访问私有属性,可以体现类与类之间的包含关系

内部类的分类

定义在外部类局部位置,如方法体内:

  • 局部内部类(有类名)
  • 匿名内部类(无类名)

定义在外部类的成员位置:

  • 成员内部类(没用 static 修饰)
  • 静态内部类(使用 static 修饰)

局部内部类

局部内部类是定义在内部类的局部位置的有类名的类,比如在方法中

注意:

  • 可以直接访问外部类的的所有成员,包括私有的
  • 不能添加访问修饰符,因为它的地位就类似一个局部变量,局部变量不可以使用访问修饰符,但是可以用 final,也就是说局部内部类可以使用 final 修饰(加 final 也就是说该内部类不能被继承)
  • 作用域仅仅在定义它的方法体或者代码块中
  • 局部内部类可以直接访问外部类的成员,就像一个类的方法内本来就可以使用它自身的其它成员
  • 外部其他类不能访问局部内部类,因为局部内部类就类似一个局部变量
  • 如果外部类成员和局部内部类成员重名,遵循就近原则,如果想访问外部类成员,则可以使用:外部类名.this.成员 访问,其中 外部类名.this 本质是 外部类的一个对象(即外部类的一个实例对象,该对象调用了包含局部内部类的方法/代码块

匿名内部类

匿名内部类是定义在内部类的局部位置的没有类名的类,比如在方法中

特点:

  • 本质是类
  • 内部类
  • 该类没有名字(表面没有,系统底层是有的)
  • 同时还是一个对象

基本语法:

new 类或接口(参数列表) {
	类体
};

场景:
使用某个接口,对应的类只想使用一次,后面不再使用(如使用容器自带的 sort 函数时,重写的 Compartor 比较器,就是一个匿名内部类)

基于接口

// 假设有接口
interface IA {
    public void eat();
}

// 使用时可以创建一个匿名内部类
public static void main(String[] args) {
		// 基于接口的匿名内部类
        IA animal = new IA() {
        	@Override
        	public void eat() {
        		System.out.println("animal is eating");
        	}
        };
        // animal 的编译类型是接口 IA
        // animal 的运行类型是匿名内部类
        // 在底层该匿名内部类其实有名字
        animal.eat();
		System.out.println("运行类型是:" + animal.getClass());	
		// 输出结果:
		// 运行类型是:class main.java.innerClass.Outer04$1
    }

在上述例子中,JDK底层在创建匿名内部类 Outer04$1 时,立即创建了 Outer04$1 实例,并且把地址返回给对象 animal
注意,匿名内部类使用一次就不能再使用,但是生成的对象 animal 是可以多次使用的

基于类

就是在创建对象的时候,在语句末尾加上一个大括号,那么就相当于创建了一个基于某个类的匿名内部类。
如:

// 有一个类
class Tiger{
    public void eat() {
        System.out.println("tiger is eating");
    }
}

// main 方法
public static void main(String[] args) {
        // 基于类的匿名内部类,注意加了大括号
        Tiger tiger = new Tiger(){
        // 在该匿名内部类底层,它其实继承了原来的类 Tiger
			 @Override
            public void eat() {
                System.out.println("重写了eat方法");
            }
        };
        System.out.println("运行类型:" + tiger.getClass());
        // 运行类型:class main.java.innerClass.AnonymousTest$1
        // 变成了一个新的类
        tiger.eat();	// tiger 编译类型是 Tiger,运行类型是 匿名内部类AnonymousTest$1

// 下面的方法效果也是一样的
//        new Tiger(){
//            @Override
//            public void eat() {
//                System.out.println("重写了eat方法");
//            }
//        }.eat();
    }

注意:

  • 在上述的例子中,如果 Tiger 是一个抽象类,就必须要实现抽象类的抽象方法,也就是说,如果基于的对象是抽象类,那么匿名内部类就必须实现所有的抽象方法
  • 基于类的匿名内部类在底层其实继承了它所基于的那个类,比如上述例子,匿名内部类继承了 Tiger

使用细节

  • 匿名内部类既是一个类的定义,同时它也是一个对象
  • 匿名内部类可以直接使用外部类的所有成员
  • 不能添加访问修饰符
  • 它的作用域仅仅在定义它的方法或者代码块中
  • 外部其他类不能访问匿名内部类(因为它相当于一个局部变量)
  • 如果外部类和匿名内部类重名,采取就近原则,若想使用外部类成员。可通过:外部类名.this.成员名,同样 外部类名.this 指的就是调用了创建该匿名内部类的方法/代码块的对象

实践场景

当作实参直接使用

public class AnonymousTest2 {
    public static void main(String[] args) {
        // 匿名内部类做实参
        func(new IB() {
            @Override
            public void show() {
                System.out.println("直接当实参,简洁高效");
            }
        });
        // 传统方法
        func(new Picture());
    }

    public static void func(IB ib) {
        ib.show();
    }
}

interface IB {
    public void show();
}

class Picture implements IB {
    @Override
    public void show() {
        System.out.println("这是一幅画");
    }
}

成员内部类

定义在外部类的成员位置,并且没有 static 修饰

  • 可以直接访问外部类的所有成员
  • 可以添加任意访问修饰符
  • 可以被其它成员调用,外部类的方法若想使用,需要先创建成员内部类对象
  • 外部其他类也可以访问成员内部类
  • 如果外部类和匿名内部类重名,采取就近原则,若想使用外部类成员。可通过:外部类名.this.成员名,同样 外部类名.this 指的就是调用了创建该匿名内部类的方法/代码块的对象

外部其他类使用成员内部类的两种方式:

public class AnonymousTest3 {
    public static void main(String[] args) {
        Outer03 outer03 = new Outer03();
        outer03.hi();
        // 第一种形式
        // outer03.new Inner01() 相当于把 new Inner01() 当作是 outer03 的成员
        Outer03.Inner01 inner01 = outer03.new Inner01();
        inner01.hello();
        // 第二种形式
        // 在外部类中编写方法返回成员内部类实例
        Outer03.Inner01 inner01Instance = outer03.gerInner03Instance();
        inner01Instance.hello();
    }
}

class Outer03 {
    private int n1 = 18;
    public String name = "jerry";

    public void hi() {
        System.out.println("hi~~~~~~~~~~~~~~");
    }
    class Inner01 {
        public void hello() {
            System.out.println("hello~~~~~~~~~~~");
        }
    }
    public Inner01 gerInner03Instance() {
        return new Inner01();
    }
}

静态内部类

定义在外部类的成员位置,并且有 static 修饰

  • 可以访问外部类的所有静态成员,但不能访问非静态成员
  • 可以添加任意访问修饰符
  • 作用域为整个类体,和正常的静态成员一样
  • 如果外部类和匿名内部类重名,采取就近原则,若想使用外部类成员。可通过:外部类名.成员名(因为静态内部类只能访问静态成员,静态成员可以通过类名调用)

外部其他类访问静态内部类的方法:

public class AnonymousTest4 {
    public static void main(String[] args) {
        // 外部其他类访问静态内部类
        // 方法一,通过类名直接访问(满足访问权限)
        Outer05.Inner02 inner02 = new Outer05.Inner02();
        inner02.eat();
        // 方法二,编写方法返回静态内部类对象实例
        Outer05 outer05 = new Outer05();
        // 调用方法返回实例
        Outer05.Inner02 inner02Instance = outer05.getInner5Instance();
        inner02Instance.eat();
        // 也可以直接调用静态方法
        Outer05.Inner02 inner02Static = Outer05.gerInner02Static();
        inner02Static.eat();
    }
}

class Outer05 {
    private int n1 = 10;
    private static String name = "jerry";
    private static void sleep() {
        System.out.println("sleeeeeeep");
    }

    static class Inner02 {
        public void eat() {
            System.out.println("静态内部类的方法~~~~~");
            sleep();
        }
    }

    public Inner02 getInner5Instance() {
        return new Inner02();
    }

    public static Inner02 gerInner02Static() {
        return new Inner02();
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

三更鬼

谢谢老板!

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

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

打赏作者

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

抵扣说明:

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

余额充值