Day12-内部类&代码块&枚举

本文详细介绍了Java中的内部类(成员、局部、静态和匿名)的定义、特点及使用场景,涉及代码块(静态代码块、构造代码块)的执行顺序,以及枚举类的定义和常见方法。同时探讨了单例模式(饿汉式和懒汉式)的应用。
摘要由CSDN通过智能技术生成

Day12-内部类&代码块&枚举

学习目标

  • 能够定义使用内部类
  • 能够说出四种不同内部类的特点
  • 能够定义并使用静态代码块
  • 能够说出类和实例对象的初始化过程
  • 能够说出代码块的调用顺序
  • 能够定义枚举并说出枚举常见方法的用法

1. 内部类

在一个类A里,我们可以定义变量,也可以定义方法。思考一下,在A类里还能在定义一个B类吗?这种语法在Java里是允许的,这种情况下,B类可以被称为内部类,A类被称为外部类。

当一个类的内部,还有一个部分需要一个完整的结构进行描述,而这个内部的完整的结构又只为外部事物提供服务,不在其他地方单独使用,那么整个内部的完整结构最好使用内部类。

根据内部类定义的方式不同,可以讲内部类分为四种:

  • 成员内部类
  • 局部内部类
  • 静态内部类
  • 匿名内部类

注意:对于普通的类来说,我们只能使用缺省或者public权限修饰符;而对于有些内部类,可以使用全部的四种权限修饰符。

1.1 成员内部类

语法格式:

[修饰符] class 外部类{
    [修饰符] class 内部类{  
    }
}

示例:

public class Test {
    public static void main(String[] args) {
        // 直接使用 new Outer().new Inner() 方法创建一个Inner对象
        Outer.Inner inner1 = new Outer().new Inner();  
        
        // 创建一个 Outer 对象,再调用 Outer 对象的 getInner方法,在getInner方法里创建并返回 Inner 对象
        Outer.Inner inner2 = new Outer().getInner();
    }
}
class Outer {
    int age = 19;
    private String name = "jack";
	
    private void test() {
        System.out.println("我是Outer类里的test方法");
    }
    
    class Inner {  // 可以把 Inner 想象成为和 age / name一样,只不过它的数据类型是class
        // static int a = 10;  报错,不能使用 static
        static final int b = 1;  // 可以使用 static final 定义常量
        private void test() {
            System.out.println("我是Inner类里的test方法");
        }
        public void demo() {
            // 内部类可以直接访问外部类的成员,包括私有成员。
            System.out.println(name);
            this.test();  // 等价于 test() 调用的是内部类的test方法
            Outer.this.test(); // 需要使用 Outer.this 调用外部类的 test 方法
        }
    }
    public static void foo() {
        // Inner inner = new Inner();  报错!外部static成员不允许访问非静态内部类
    }
    public Inner getInner() {
        return new Inner();
    }
}

成员内部类特点:

  1. 不能使用 static关键字,但是可以使用static final关键字定义常量。
  2. 内部类可以直接访问外部类的成员(包括私有成员)。
  3. 内部类如果想要调用外部类的方法,需要使用外部类名.this.外部方法名
  4. 可以使用final修饰内部类,表示不能被继承。
  5. 编译以后也会有自己独立的字节码文件,只不过文件名是Outer$Inner.class
  6. 外部函数的static成员里,不允许访问成员内部类。
  7. 可以在外部类里创建内部类对象然后再通过方法返回,同时,也可以使用new Outer().new Inner()在外部直接访问内部类。

1.2 局部内部类

局部内部类的使用和成员内部类的使用基本一致,只是局部内部类定义在外部类的方法中,就像局部变量一样,并不是外部类的成员。局部内部类在方法外是无法访问到的,但它的实例可以从方法中返回,并且实例在不再被引用之前会一直存在。

语法格式:

[修饰符] class 外部类{
    [修饰符] 返回值类型  方法名([形参列表]){
            [final/abstract] class 内部类{
    	}
    }    
}

示例:

class Outer {
    int age = 19;
	static int m = 10;
    
    public void test() {
        // final String y = "good";
        String y = "good";  // JDK8 以后,final可以省略
        class Inner {  // 定义在外部类的某个方法里
            // static int a = 10; 不能定义静态变量!  
            private  void demo() {
                System.out.println(age);
                // 不能修改外部函数的局部变量
				// y = 'yes';
                // 只能访问被 final 修饰的外部函数的局部变量
                // JDK8 以后,如果外部函数的局部变量没有加 final,编译器会自动加 final
                System.out.println(y);
            }
        }
        Inner inner = new Inner();
        inner.demo();
    }
}

局部内部类特点:

  1. 定义在类的某个方法里,而不是直接定义在类里。
  2. 局部内部类前面不能有权限修饰符!
  3. 局部内部类里不能使用static声明变量!
  4. 局部内部类中能访问外部类的静态成员。
  5. 如果这个局部内部类所在的方法是静态方法,它无法访问访问外部类的非静态成员。
  6. 局部内部类可以访问外部函数的局部变量,但是这个局部变量必须要被final修饰。JDK8以后,如果局部变量被局部内部类使用了,会自动在前面加final.

思考: 为什么局部变量前要加final.

public class TestInner{
	public static void main(String[] args) {
		A obj = Outer.method();
		//因为如果c不是final的,那么method方法执行完,method的栈空间就释放了,那么c也就消失了
		obj.a();//这里打印c就没有中可取了,所以把c声明为常量,存储在方法区中
	}
}

interface A{
	void a();
}
class Outer{
	public static A method(){
		final int c = 3;
		class Sub implements A{
			@Override
			public void a() {
				System.out.println("method.c = " + c);
			}
		}
		return new Sub();
	}
}

1.3 匿名内部类

语法格式:

new 父类名或者接口名(){
    // 方法重写
    @Override 
    public void method() {
        // 执行语句
    }
};

实例:

abstract class Animal {
    abstract void shout();
}
class Demo {
    public void demo() {
        Animal animal = new Animal(){
            @Override
            void shout() {
                System.out.println("动物正在叫");
            }
        };
    }
}

匿名内部类的特点:

  1. 匿名内部类就是一种特殊的局部内部类,只不过没有名称而已,它的基本特点和局部内部类一致。
  2. 匿名内部类不能有构造器,匿名内部类没有类名,肯定无法声明构造器。
  3. 匿名内部类的前提是,这个内部类必须要继承自一个父类或者父接口!
  4. 匿名内部类是接口的一种常见简化写法,也是我们开发中最常使用的一种内部类。它的本质是一个实现了父类或者父接口具体方法的一个匿名对象。

1.4 静态内部类

静态内部类也被称为嵌套类,不同于前三种内部类,静态内部类不会持有外部类对象的引用。

语法格式:

[修饰符] class 外部类{    
    [其他修饰符] static class 内部类{    }
}

示例:

class Outer {
    int age = 19;
    static String type = "outer";

    static class Inner {  // 需要使用 static 关键字
        static int x = 1;  // 能够定义静态变量
        public static void test() {
            // 只能访问外部的静态变量,不能访问非静态变量
            // System.out.println(age);
            System.out.println(type);
        }
    }
    public void  demo() {
        System.out.println(Inner.x);  // 外部类可以直接通过 内部类.静态变量名,不需要创建对象
    }
}

静态内部类特点:

  1. 使用static关键字修饰。
  2. 在静态内部类里,可以使用static关键字定义静态成员。
  3. 只能访问外部的静态成员,不能访问外部的非静态成员。
  4. 外部类可以直接通过静态内部类名.静态成员名访问静态内部类的静态成员。

其实严格的讲(在James Gosling等人编著的《The Java Language Specification》)静态内部类不是内部类,而是类似于C++的嵌套类的概念,外部类仅仅是静态内部类的一种命名空间的限定名形式而已。所以接口中的内部类通常都不叫内部类,因为接口中的内部成员都是隐式是静态的。例如:Map.Entry。

2. 代码块

Java中包括四种代码块:

  1. 普通代码块:就是类里方法体代码。
  2. 构造代码块:类里直接使用 {} 编写的代码。
  3. 静态代码块:在构造代码块前添加static关键字。
  4. 同步代码块:使用synchronize关键字包裹起来的代码块,用于多线程。

普通代码块就是方法体的代码,这里就不再赘述。下面我们来看一下构造代码块和静态代码块。

2.1 静态代码块

语法结构:

[修饰符] class 类名{
    static{
        静态代码块语句;
    }
}

示例:

public class Demo {
    public static void main(String[] args) {
        System.out.println("我是Main方法");
        Demo d = new Demo();
    }
    Demo() {
        System.out.println("我是构造方法");
    }
    
    private static int a;
    static {
        a = 10;  // 静态代码块可以用于对静态变量初始化
        System.out.println("我是静态代码块");
    }
}

特点:

  1. 使用static关键字修饰,写在类里,方法外,用来对类进行初始化。
  2. 一个类里可以有多个静态代码块,但是通常情况下只会定义一个。
  3. 随着类加载而执行,只执行一次,只要类加载就会执行,所以执行的优先级很高。
  4. 一般情况下,如果有些代码必须在项目启动的时候就执行的时候,就需要使用静态代码块。

2.2 类的初始化

类被加载内存后,会在方法区创建一个Class对象(后面反射章节详细学习)来存储该类的所有信息。此时会为类的静态变量分配内存,然后为类变量进行初始化。在加载类的过程中,JVM会调用<clinit>方法,专门来对类变量或者静态代码块进行初始化。

public class Test{
    public static void main(String[] args){
    	Father.test();
    }
}
class Father{
	private static int a = getNumber();
	static{
		System.out.println("Father(1)");
	}
	private static int b = getNumber();
	static{
		System.out.println("Father(2)");
	}
	
	public static int getNumber(){
		System.out.println("getNumber()");
		return 1;
	}
	
	public static void test(){
		System.out.println("Father:test()");
	}
}

运行结果:
getNumber()
Father(1)
getNumber()
Father(2)
Father:test()

在这里插入图片描述

public class Test{
    public static void main(String[] args){
    	Son.test();
        System.out.println("-----------------------------");
        Son.test();
    }
}
class Father{
	private static int a = getNumber();
	static{
		System.out.println("Father(1)");
	}
	private static int b = getNumber();
	static{
		System.out.println("Father(2)");
	}
	
	public static int getNumber(){
		System.out.println("Father:getNumber()");
		return 1;
	}
}
class Son extends Father{
	private static int a = getNumber();
	static{
		System.out.println("Son(1)");
	}
	private static int b = getNumber();
	static{
		System.out.println("Son(2)");
	}
	
	public static int getNumber(){
		System.out.println("Son:getNumber()");
		return 1;
	}
	
	public static void test(){
		System.out.println("Son:test()");
	}	
}
// 运行结果
Father:getNumber()
Father(1)
Father:getNumber()
Father(2)
Son:getNumber()
Son(1)
Son:getNumber()
Son(2)
Son:test()
-----------------------------
Son:test()

2.3 构造代码块

语法结构:

{
    //代码块里的内容
}

示例:

package com.atguigu.java;

public class Demo {
    public static void main(String[] args) {
        System.out.println("我是main方法");

        Demo d1 = new Demo();
        System.out.println(d1.getName());

        Demo d2 = new Demo();

    }

    public String getName() {
        return name;
    }

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

    private String name;

    public Demo() {
        System.out.println("我是构造方法");
    }
    {
        System.out.println("我是构造代码块");
        this.name = "jack";
    }
}
// 我是main方法
// 我是构造代码块
// 我是构造方法
// jack
// 我是构造代码块
// 我是构造方法

特点:

  1. 对象一建立就会执行构造代码块,构造代码块先于构造方法执行,用来给对象进行初始化。
  2. 每创建一个对象都会调用一次构造代码块。
  3. 构造代码块是给所有对象统一初始化,构造函数是给各自对应的对象初始化。
  4. 如果每个实例对象都需要统一的初始化,可以考虑将这部分代码写入到构造代码块里。

2.4 实例对象的初始化

除了<clinit>方法初始化类以外,在创建实例对象时,还会自动调用<init>方法对实例对象进行初始化。

使用一个类来实例化对象的几种方式:

  1. 最常见的方式,使用关键字new来创建一个实例对象。
  2. 使用反射,调用Class或者java.lang.reflect.Constructor对象的newInstance()方法。
  3. 调用对象的clone()方法。
  4. 调用java.io.ObjectInputStream类的getOjbect()方法序列化。
public class Test{
    public static void main(String[] args){
    	Father f1 = new Father();
    	Father f2 = new Father();
    }
}
class Father{
	private int a = getNumber();
	private String info;
	{
		System.out.println("Father(1)");
	}
	Father(){
		System.out.println("Father()无参构造");
	}
	Father(String info){
		this.info = info;
		System.out.println("Father(info)有参构造");
	}
	private int b = getNumber();
	{
		System.out.println("Father(2)");
	}
	
	public int getNumber(){
		System.out.println("Father:getNumber()");
		return 1;
	}
}
运行结果:
Father:getNumber()
Father(1)
Father:getNumber()
Father(2)
Father()无参构造
Father:getNumber()
Father(1)
Father:getNumber()
Father(2)
Father(info)有参构造

在这里插入图片描述

2.5 代码块的执行顺序

先初始化类,加载类变量(static类型变量和static代码块),再调用构造代码块,然后再调用构造方法。

class Father {
    Father() {
        System.out.println("我是Father里的构造方法");
    }
    static {
        System.out.println("我是Father里的静态代码块");
    }
    {
        System.out.println("我是Father里的构造代码块");
    }
}

public class Demo extends Father {
    public static void main(String[] args) {
        System.out.println("我是Main方法");
        Demo d = new Demo();
    }
    Demo() {
        System.out.println("我是Demo构造方法");
    }
    static {
        System.out.println("我是Demo静态代码块");
    }
    {
        System.out.println("我是Demo构造代码块");
    }
}

// 我是Father里的静态代码块
// 我是Demo静态代码块
// 我是Main方法
// 我是Father里的构造代码块
// 我是Father里的构造方法
// 我是Demo构造代码块
// 我是Demo构造方法

3. 单例设计模式

单例模式,是一种常用的软件设计模式,在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中应用该模式的类一个类只有一个实例,即一个类只有一个对象实例。例如,windows操作系统里的回收站。

步骤:

  1. 将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象。
  2. 在该类内部产生一个唯一的实例化对象,并且将其封装为private static final类型。
  3. 定义一个静态方法返回这个唯一对象。

单例设计模式分为饿汉式(立即加载型)和懒汉式(延迟加载型)。由于懒汉式有线程安全问题,又衍生出了线程安全版的懒汉式以及DLC双检查锁(最佳实现方式)的懒汉式。

3.1 饿汉式

立即加载就是使用类的时候已经将对象创建完毕(不管以后会不会使用到该实例化对象,先创建了再说。很着急的样子,故又被称为“饿汉模式”),常见的实现办法就是直接new实例化。

public class Singleton {

    // 将自身实例化对象设置为一个属性,并用static修饰
    private static final Singleton instance = new Singleton();
    
    // 构造方法私有化
    private Singleton() {}
    
    // 静态方法返回该实例
    public static Singleton getInstance() {
        return instance;
    }
}
  • 优点:实现起来简单,没有多线程同步问题。
  • 缺点:当类SingletonTest被加载的时候,会初始化static的instance,静态变量被创建并分配内存空间,从这以后,这个static的instance对象便一直占着这段内存(即便你还没有用到这个实例),当类被卸载时,静态变量被摧毁,并释放所占有的内存,因此在某些特定条件下会耗费内存。

3.2 懒汉式

延迟加载就是调用get()方法时实例才被创建(先不急着实例化出对象,等要用的时候才给你创建出来。不着急,故又称为“懒汉模式”),常见的实现方法就是在get方法中进行new实例化。

public class Singleton {

    // 将自身实例化对象设置为一个属性,并用static修饰
    private static Singleton instance;
    
    // 构造方法私有化
    private Singleton() {}
    
    // 静态方法返回该实例
    public static Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
  • 优点:实现起来比较简单,当类SingletonTest被加载的时候,静态变量static的instance未被创建并分配内存空间,当getInstance方法第一次被调用时,初始化instance变量,并分配内存,因此在某些特定条件下会节约了内存。
  • 缺点:在多线程环境中,这种实现方法是完全错误的,根本不能保证单例的状态。

4. 枚举类

有些时候,如果一个类的对象是有限并且固定的,可以考虑使用枚举类。在JDK1.5之前,需要自己手动的实现。

public class Test {
    public static void main(String[] args) {
        System.out.println(Season.SPRING);
    }
}

class Season {
    private Season() {
    }
    public static final Season SPRING = new Season();
    public static final Season SUMMER = new Season();
    public static final Season AUTUMN = new Season();
    public static final Season WINTER = new Season();
}

4.1 Enum的使用

枚举(enum)类型是Java 5新增的特性,它是一种新的类型,允许用常量来表示特定的数据。有了枚举,可以把相关的常量分组到一个枚举类型里,而且枚举提供了比常量更多的方法。

语法格式:

enum 枚举类类名 {
    对象1,对象2,对象3;
}

示例:

public enum Season{
    // SPRING(),SUMMER(),AUTUMN(),WINTER();  调用构造函数,创建了四个对象,小括号可以省略
    
    SPRINT,SUMMER,AUTUMN,WINTER;  // 创建了四个对象
    // private Season(){} 构造方法可以不写,默认就有一个空参数构造方法
}

public enum WeekDay {
    // 创建的实例对象时,必须要调用构造方法传入 name 参数
    MON("周一"), TUE("周二"), WED("周三"), THU("周四"), FRI("周五"), SAT("周六"), SUN("周日");
    
    private String name;
    WeekDay(String name) {  // 还能自定义构造方法
        this.name = name;
    }
    
    // 像正常的类一样,可以使用 getter/setter 方法
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

枚举特点:

  1. 所有的枚举都继承自java.lang.Enum类,由于Java 不支持多继承,所以枚举对象不能再继承其他类(但是可以实现接口)。
  2. 枚举类的所有实例对象都必须放在第一行展示,并且默认都是以public static final修饰的常量,所以变量名通常都全大写。
  3. 在创建实例对象时,不需使用new 关键字,也不需使用小括号显式调用构造器
  4. 使用enum定义、非抽象的枚举类默认使用final修饰,不可以被继承。
  5. 枚举类的构造器只能是私有的,不允许在外部创建对象。

4.2 常见方法

方法名作用
toString返回的是常量名(对象名),可以重写
name返回的是常量名(对象名),不推荐使用,建议使用toString
values返回该枚举类的所有的常量对象,返回类型是当前枚举的数组类型。
valueOf根据常量名,返回一个枚举对象。
ordinal返回枚举常量的序数(它在枚举声明中的位置,其中初始常量序数为零)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值