# 1. 接口

# 1.1 概念

​ 主要用来描述类具有什么功能,而并不给出每个功能的具体实现。一个类可以实现( implement)—个或多个接口,并在需要接口的地方, 随时使用实现了相应接口的对象。

​ 接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。

举例:Arrays 类中的 sort 方法承诺可以对对象数组进行排序,但要求满足下列前提:对象所属的类必须实现了 Comparable 接口。

# 1.2 接口的特性

  • 接口不能使用 new 运算符实例化
  • 接口中的方法都自动地被设置为 public
  • 接口中的域将被自动设为 public static final
  • 尽管每个类只能够拥有一个超类, 但却可以实现多个接口。这就为定义类的行为提供了极大的灵活性

# 1.3 接口与抽象类

为什么 Java 程序设计语言还要不辞辛苦地引入接口概念? 为什么不用抽象类实现呢?

其实使用抽象类是存在局限性的,因为一个类只能继承一个类,当某个类以及存在了父类,但是又想给这个类扩展一定的功能。无法继承第二个类了。所有才有类接口的概念,一个类可以实现多个接口,接口同时还可以能避免多重继承的复杂性和低效性。

# 1.4 抽象方法

接口中的方法默认是publicabstract的,即使你不显式地使用这些修饰符。这意味着这些方法没有具体实现,必须由实现了该接口的类来提供具体的实现。

# 1.5 静态方法

同样从Java 8起,接口也可以包含静态方法。静态方法属于接口本身,而不属于实现接口的类。

# 1.6 默认方法

从Java 8开始,接口可以有带有实现的方法,称为默认方法。默认方法允许你向现有接口添加新功能而不会破坏实现该接口的现有代码。

# 1.7 变量

接口可以定义字段,这些字段默认是publicstaticfinal的,即它们是常量。

# 2.内部类

# 1、内部类简介

内部类( inner class) 是定义在另一个类中的类。为什么需要使用内部类呢? 其主要原因

  • 内部类方法可以访问该类定义所在的作用域中的数据, 包括私有的数据。
  • 内部类可以对同一个包中的其他类隐藏起来。
  • 当想要定义一个回调函数且不想编写大量代码时,使用匿名 (anonymous) 内部类比较便捷

Java 内部类 (opens new window)(Inner Class)是定义在另一个类也就是外部类(Outer Class)内部的类。内部类又称为嵌套类(Nested Class),外部类 (opens new window)又称为封闭类(Enclosing Class)。

# 2、内部类的分类

Java 中有四种内部类:

  1. 成员内部类(Member Inner Class)
    定义在外部类的成员位置上,与成员变量和方法平级。
  2. 静态内部类(Static Nested Class)
    使用static关键字修饰。
  3. 局部内部类(Local Inner Class)
    定义在一个类的局部位置上,例如成员方法和代码块内。
  4. 匿名内部类(Anonymous Inner Class)
    没有名字的内部类,常用于简化代码。

# 1、成员内部类

1、介绍

成员内部类是定义在一个外部类的成员位置的的类,没有 static 修饰。

示例:

class OuterClass {
	private String outerClassField = "outer class's field~";
 
	class MemberInnerClass { // 成员内部类
		private String memberInnerClassField = "member inner class's field";
	}
}
1
2
3
4
5
6
7

2、特点

1)在成员内部类中访问外部类的成员,在成员内部类中能访问外部类的所有成员,包括私有的。

i.方法一:直接访问

直接使用外部类的成员的名字访问:

class OuterClass {
	private String outerClassField = "outer class's field~";
 
	class MemberInnerClass { // 成员内部类
		public void foo() {
			System.out.println(outerClassField); // 直接使用成员名访问
		}
	}
}
1
2
3
4
5
6
7
8
9

ii.方法二:使用外部类的 this 引用访问

class OuterClass {
	private String outerClassField = "outer class's field~";
 
	class MemberInnerClass { // 成员内部类
		public void foo() {
			System.out.println(OuterClass.this.outerClassField);// 使用外部类的this引用访问
		}
	}
}
1
2
3
4
5
6
7
8
9

这个方法可以在外部类的成员与成员内部类的成员重名时使用。因为如果外部类成员与成员内部类成员重名,在成员内部类中直接使用这个成员的名字访问的成员是成员内部类的成员(遵循就近原则),如果想要访问外部类的这个成员,就要使用上面提到的方法。

格式:

`外部类名.this.成员名`
1

示例:

class OuterClass {
	private int a;
 
	class MemberInnerClass { // 成员内部类
		private int a;
		public void foo() {
			System.out.println("member inner class's field: " + a);
			System.out.println("outer class's field: " + OuterClass.this.a);
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11

2)在外部类中访问成员内部类的成员

在外部类中可以访问成员内部类的所有成员,包括私有的。

class OuterClass {
	class MemberInnerClass { // 成员内部类
		private String privateField = "member inner class's private field~";
		
		private void privateMethod() {
			System.out.println("member inner class's private method~");
		}
	}
	
	public void fun() {
		MemberInnerClass mic = new MemberInnerClass();
		
		mic.privateMethod(); // 外部类访问成员内部类的私有方法
 
		System.out.println(mic.privateField); // 外部类访问成员内部类的私有属性
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

在外部类中,通过成员内部类的实例,可以访问成员内部类的所有成员,包括私有的。

3、可以添加任何访问修饰符

成员内部类可以添加任何访问修饰符,因为它也是外部类的成员。

class OuterClass {
	public class MemberInnerClassA {
		
	}
 
	protected class MemberInnerClassB {
 
	}
 
	class MemberInnerClassC {
 
	}
 
	private class MemberInnerClassD {
 
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

4、在其他类中创建成员内部类的实例

在其他类中创建成员内部类的实例的前提需要保证成员内部类的可见性是在这个其他类中可见的。而且由于成员内部类时外部类的实例成员,所以如果想要创建成员内部类的实例,首先需要创建外部类的实例,然后通过外部类的实例来创建成员内部类的实例。

方法一:

先创建一个外部类实例,然后使用这个外部类实例创建成员内部类的实例:

public class Example {
	public static void main(String[] args) {
		// 方法一
		OuterClass oc = new OuterClass();
		OuterClass.MemberInnerClass mic = oc.new MemberInnerClass();
	}
}
 
class OuterClass {
	class MemberInnerClass {
		
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13

或者,创建外部类的实例的同时创建成员内部类的实例:

public class Example {
	public static void main(String[] args) {
		// 方法二
		OuterClass.MemberInnerClass mic = new OuterClass().new MemberInnerClass();
	}
}
 
class OuterClass {
	class MemberInnerClass {
 
	}
}
1
2
3
4
5
6
7
8
9
10
11
12

方法二:

在外部类中定义一个方法,用来创建成员内部类的实例,然后返回这个实例:

public class Example {
	public static void main(String[] args) {
		// 方法三
		OuterClass.MemberInnerClass mic = new OuterClass().getMemberInnerClassInstance();
	}
}
 
class OuterClass {
	class MemberInnerClass {
 
	}
	
	public MemberInnerClass getMemberInnerClassInstance() {
		return new MemberInnerClass();
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

5、编译器为成员内部类生成的类名

成员内部类在编译后会生成一个独立的类文件,类文件的命名通常是 外部类名$成员内部类名.class

我们可以通过 getClass() 方法来查看编译器为这个成员内部类生成的类名:

public class Example {
	public static void main(String[] args) {
		OuterClass oc = new OuterClass();
		oc.foo();
	}
}
 
class OuterClass {
	class MemberInnerClass {
 
	}
 
	public void foo() {
		MemberInnerClass mic = new MemberInnerClass();
 
		System.out.println(mic.getClass());
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

运行结果:

类名格式一般是:外部类名$成员内部类名,这是编译器产生的,用于确保在字节码中能够唯一标识这个成员内部类。编译器生成的名称是该成员内部类的真正类名,而在代码中使用的名称则是一个标识符,用于在源代码中引用这个类。

6、补充

  1. 成员内部类的作用域为整个外部类类体,因为成员内部类的地位是外部类的实例成员。

  2. 由于成员内部类是外部类的实例成员,所以成员内部类的实例是需要基于外部类的实例来创建的,如果外部类没有创建实例,成员内部类是不能创建实例的。
    也就是说,如果你尝试在一个静态的块中创建一个成员内部类,就会导致报错。就如下面这样:

    class OuterClass {
    	class MemberInnerClass {
    		
    	}
     
    	public static void fun() {
    		MemberInnerClass mic = new MemberInnerClass(); 
    		// 这里会报错,因为静态函数是属于类的,
    		// 而不是对象的,所以没有外部类的对象这个函数也能调用,
    		// 又由于成员内部类要基于外部类的实例来创建,
    		// 所以这里无法创建内部类实例
    	}
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    这样就引出了下一个内部类,也就是静态内部类。

# 2、静态内部类

1、介绍

静态内部类是定义在一个外部类的成员位置的的类,有 static 修饰。

示例:

class OuterClass {
	static class StaticNestedClass { // 静态内部类
 
	}
}
1
2
3
4
5

它的地位就相当于外部类的一个静态成员。

2、特点

1)在静态内部类中访问外部类的成员

在静态内部类中只能访问外部类的静态成员,包括私有的。

i.方法一:直接访问

直接使用外部类的静态成员名来访问这个静态成员:

class OuterClass {
	private static String outerClassField = "outer class's private field~";
 
	static class StaticNestedClass { // 静态内部类
		public void fun() {
			System.out.println(outerClassField); // 直接使用外部类的静态成员名访问这个成员
		}
	}
}

1
2
3
4
5
6
7
8
9
10

ii.方法二:使用外部类的类名访问

使用外部类的类名加这个静态成员名来访问这个静态成员:

class OuterClass {
	private static String outerClassField = "outer class's private field~";
 
	static class StaticNestedClass { // 静态内部类
		public void fun() {
			System.out.println(OuterClass.outerClassField);
			// 通过外部类名加静态成员名访问这个静态成员
		}
	}
}
1
2
3
4
5
6
7
8
9
10

这里因为是静态内部类,所以这个静态内部类是属于这个外部类整个类的,而不是属于某个外部类的实例的,所以这个静态内部类中没有外部类当前实例的 this 引用。同时,静态内部类也只能访问外部类的静态成员。

所以这里使用的是外部类名加静态成员名来访问。

格式:

`外部类名.静态成员名`
1

同时,这个方式可以在静态内部类中的成员与外部类的某个静态成员重名时使用,因为由于就近原则,在静态内部类中直接使用重名的成员的名字访问到的是静态内部类中的成员,如果想要访问外部类的这个静态成员,就需要使用上面的格式来指定。

示例:

class OuterClass {
	private static int a = 10;
 
	static class StaticNestedClass {
		private int a = 100;
 
		public void fun() {
			System.out.println(a); // 访问的是静态内部类中的成员
			System.out.println(OuterClass.a); // 访问的是外部类中的静态成员
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11
12

2)在外部类中访问静态内部类中的成员

在外部类中可以访问静态内部类中的所有成员,包括私有的。

class OuterClass {
	static class StaticNestedClass {
		private String staticNestedClassField = "static nested class's field";
	}
	
	public void fun() {
		StaticNestedClass snc = new StaticNestedClass();
 
		System.out.println(snc.staticNestedClassField);
	}
}
1
2
3
4
5
6
7
8
9
10
11

在外部类中,通过静态内部类的实例可以访问静态内部类的所有成员,包括私有的。

3)可以添加任何访问修饰符

由于静态内部类的地位就是外部类的静态成员,所以可以添加访问修饰符,用来决定它的对外可见度。

class OuterClass {
	public static class StaticNestedClassA {
		
	}
	
	protected static class StaticNestedClassB {
		
	}
	
	static class StaticNestedClassC {
		
	}
	
	private static class StaticNestedClassD {
		
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

4)在其他类中创建静态内部类的实例

在其他类中创建静态内部类的实例需要保证静态内部类的对外可见性在这个其他类中是可见的。

方法一:

可以直接使用外部类的类名来调用静态内部类的构造器来创建静态内部类的实例。

public class Example {
	public static void main(String[] args) {
		OuterClass.StaticNestedClass snc = new OuterClass.StaticNestedClass();
 
	}
}
 
class OuterClass {
	static class StaticNestedClass {
		
	}
}
1
2
3
4
5
6
7
8
9
10
11
12

方法二:

可以定义一个静态方法用来创建一个静态内部类的实例并返回。

public class Example {
	public static void main(String[] args) {
		OuterClass.StaticNestedClass snc = OuterClass.getStaticNestedClassInstance();
		
	}
}
 
class OuterClass {
	static class StaticNestedClass {
 
	}
	
	public static StaticNestedClass getStaticNestedClassInstance() {
		return new StaticNestedClass();
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

5)编译器为静态内部类生成的类名

静态内部类在编译后会生成一个独立的类文件,类文件的命名通常是 外部类名$静态内部类名.class

我们可以通过 getClass() 方法来查看编译器为这个静态内部类生成的类名:

public class Example {
	public static void main(String[] args) {
		OuterClass.foo();
	}
}
 
class OuterClass {
	static class StaticNestedClass {
 
	}
 
	public static void foo() {
		StaticNestedClass snc = new StaticNestedClass();
 
		System.out.println(snc.getClass());
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

运行结果:

类名一般为:外部类名$静态内部类名,这是编译器产生的,用于确保在字节码中能够唯一标识这个静态内部类。编译器生成的名称是该静态内部类的真正类名,而在代码中使用的名称则是一个标识符,用于在源代码中引用这个类。

3、补充

  1. 由于静态内部类相当于外部类的一个静态成员,所以它是属于外部类整个类的,而不是属于某个外部类实例的,所以它不需要基于外部类的实例来进行实例化,直接使用外部类的类名就可以调用它的构造器来进行实例化。
    同时,由于它不是实例成员,它的内部也不在内嵌有外部类当前实例的 this 引用。它也无法访问外部类的非静态成员。
  2. 静态内部类的作用域是外部类整个类体,因为它的地位是外部类的静态成员。

# 3、局部内部类

1、介绍

局部内部类定义在方法、构造器或代码块内部,类似于方法的局部变量。它的作用域仅限于定义它的块中。

class OuterClass {
	public OuterClass() {
		class LocalInnerClassA { // 在构造器中定义局部内部类
 
		}
	}
 
	{
		class LocalInnerClassB { // 在实例代码块中定义局部内部类
 
		}
	}
 
	static {
		class LocalInnerClassC { // 在静态代码块中定义局部内部类
 
		}
	}
 
	public void fun() {
		int localVar;
 
		class LocalInnerClassD { // 在成员函数中定义局部内部类
 
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

同时,局部内部类本质也是类,所以大部分一般类的成员在局部内部类中都能有,甚至可以进行继承等操作。

2、特点

1)在局部内部类中访问外部类的成员

在局部内部类内可以直接访问到外部类的所有成员,包括私有的成员。

i.方法一:直接访问

直接通过成员名来访问具体的外部类的成员。

class OuterClass {
	private String outerField = "outer class private field~";
 
	public void fun() {
		class LocalInnerClass {
			public void foo() {
				System.out.println(outerField);// 直接访问
			}
		}
 
	}
}
1
2
3
4
5
6
7
8
9
10
11
12

ii.方法二:通过外部类的 this 引用访问

局部内部类也可以通过外部类的 this (当前实例的引用)引用来访问外部类的成员。

class OuterClass {
	private String outerField = "outer class private field~";
 
	public void fun() {
		class LocalInnerClass {
			public void foo() {
				System.out.println(OuterClass.this.outerField);// 通过外部类的this引用访问
			}
		}
 
	}
}
1
2
3
4
5
6
7
8
9
10
11
12

这个方法可以在外部类的成员与局部内部类的成员重名时使用。因为如果外部类成员与局部内部类成员重名,在局部内部类中直接使用这个成员的名字访问的成员是局部内部类的成员(遵循就近原则),如果想要访问外部类的这个成员,就要使用上面提到的方法。

格式为:

`外部类名.this.成员名`
1

示例:

class OuterClass {
	private int a = 100;
	
	public void fun() {
		class LocalInnerClass {
			private int a = 200;
			
			public void foo() {
				System.out.println("LocalInnerClass's field: " + a);
				System.out.println("outerClass's field: " + OuterClass.this.a);
			}
		}
 
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

iii.注意

需要注意的是,如果局部内部类定义在一个静态的局部中(例如静态代码块和静态方法)中,则在局部内部类中就不能访问到外部类的所有非静态成员。

2)在外部类定义局部内部类的块中访问局部内部类的成员

在外部类定义局部内部类的块中也可以访问局部内部类的所有成员,包括私有的成员。

如何访问:

在外部类定义局部内部类的块中需要使用局部内部类的实例来访问局部内部类的成员。

需要注意的是,局部内部类是在方法内部定义的,因此它的实例化和使用必须在定义它的那个方法或其内部的代码块中进行,而且是需要再这个局部内部类的定义语句之后,因为它的作用域就从它的定义开始,直到这个方法的结束。

class OuterClass {
	public void fun() {
		class LocalInnerClass {
			private String innerfield = "Hello";
		}
 
		LocalInnerClass lic = new LocalInnerClass();
		System.out.println(lic.innerfield);// 通过局部内部类的实例来访问其成员
 
	}
}
1
2
3
4
5
6
7
8
9
10
11

3)局部内部类中访问方法的局部变量

在局部内部类中可以直接访问定义它的方法中的局部变量或者是该方法的参数,或者访问定义它的代码块的局部变量。但是访问这些局部变量的要求是,这些局部变量必须是显式 final 的(直接显式使用 final 修饰)或者是隐式 final 的(等价于 final 的,即在初始化后没有被修改,这个只有在 Java 8 及之后有效)。

访问方法中的局部变量:

class OuterClass {
	public void fun() {
		final int finalLocalVar = 0;
		int neverChangedVar = 0;
		// neverChangedVar = 1;
		// 这里如果有这一个语句的话,后面就会报错,因为这样这个局部变量就不是隐式final的了
		class LocalInnerClass {
			public void foo() {
				System.out.println(finalLocalVar);
				System.out.println(neverChangedVar);
			}
		}
 
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

访问方法的参数:

class OuterClass {
	public void fun(final int finalParameter, int neverChangedParameter) {
		// neverChangedParameter = 0;
		// 这里如果有这一句的话,也会报错,因为这样这个参数就不是隐式final的了
		class LocalInnerClass {
			public void foo() {
				System.out.println(finalParameter);
				System.out.println(neverChangedParameter);
			}
		}
 
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13

访问代码块的局部变量:

class OuterClass {
	{
		final int finalLocalVar = 0;
		int neverChangedVar = 0;
		// neverChangedVar = 1;
		// 这里如果有这一个语句的话,后面就会报错,因为这样这个局部变量就不是隐式final的了
		class LocalInnerClass {
			public void foo() {
				System.out.println(finalLocalVar);
				System.out.println(neverChangedVar);
			}
		}
 
	}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

4)编译器为局部内部类生成的类名

 局部内部类在编译后会生成一个独立的类文件,类文件的命名通常是 外部类名$1局部内部类名.class,其中1表示这是这个外部类中第一个局部内部类。

我们可以通过 getClass() 方法来查看编译器为这个局部内部类生成的类名:

public class Example {
	public static void main(String[] args) {
		new OuterClassA().foo();
	}
}
 
class OuterClassA {
	public void foo() {
		class LocalInnerClassA {
			
		}
		
		LocalInnerClassA lic = new LocalInnerClassA();
		System.out.println(lic.getClass());
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

运行结果:

类名一般为:外部类名$1局部内部类名,其中1表示这是这个外部类中第一个局部内部类。这是编译器产生的,用于确保在字节码中能够唯一标识这个局部内部类。编译器生成的名称是该局部内部类的真正类名,而在代码中使用的名称则是一个标识符,用于在源代码中引用这个类。

3、补充

  1. 由于局部内部类的地位就相当于局部变量,所以它不能添加访问修饰符,但能用 final 修饰。
  2. 局部内部类的作用域只在定义它的块中。
  3. 在局部内部类中可以访问外部类的所有成员(包括私有的),同时在外部类定义了局部内部类的块中也可以访问局部内部类的所有成员(包括私有的)。
  4. 局部内部类不能包含静态成员(静态属性、静态方法、静态代码块和静态内部类),因为静态内部类的作用域仅限于方法内部,而静态成员通常是与类本身关联的。
    在 JDK 16 时,加了一个局部内部类中可以有的静态成员,就是静态属性,同时有一个限制,这个静态属性必须是 final 修饰的(这里有一个细节,static 和 final 同时使用,编译器有优化,这样这个属性就是编译时常量,不是在类加载时初始化,而是在编译时初始化)。其他的静态成员依然不能在局部内部类中出现。

# 4、匿名内部类

1、介绍

匿名内部类是一种特殊的内部类,它没有显式的类名。匿名内部类通常用于创建一个类的实例,而这个类只需要使用一次。匿名内部类可以继承一个类或实现一个接口,并且可以在定义类的内容的同时创建对象。

1)语法:

匿名内部类继承类语法:

new 父类构造器(参数列表) {
    // 类体
};
1
2
3

匿名内部类实现接口语法:

new 接口名() {
    // 类体
};

1
2
3
4

2)示例:

匿名内部类实现接口:

interface Interface {
	void cry();
}
 
class OuterClass {
	public void method() {
		Interface cat = new Interface() {
			@Override
			public void cry() {
				System.out.println("Meow~");
			}
		};// 这里就是一个匿名内部类,
		  // 这个匿名内部类实现了Interface接口,
		  // 定义了类的结构后就被实例化了
 
		cat.cry();
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

 匿名内部类继承抽象类:

abstract class Animal {
	private String name;
 
	public Animal(String name) {
		this.name = name;
	}
 
	abstract void cry();
}
 
class OuterClass {
	public void method() {
		Animal cat = new Animal("cat") {
			@Override
			public void cry() {
				System.out.println("Meow~");
			}
		};// 这里就是一个匿名内部类,
		  // 这个匿名内部类继承了抽象类Animal,然后实现了抽象类的抽象方法,
		  // 定义了类的结构后就被实例化了
 
		cat.cry();
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

 匿名内部类继承一般类:

class Animal {
	public void cry() {
		System.out.println("动物叫~");
	}
}
 
class OuterClass {
	public void fun() {
		Animal cat = new Animal() {
			@Override
			public void cry() {
				System.out.println("喵~");
			}
		};
		
		cat.cry();
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

2、特点

1)匿名类的类名

 匿名内部类只是没有显式的类名,但实际上有隐式的名字,一般命名规则是 外部类名$1 ,这里的 1 是指在这个外部类内定义的第一个匿名内部类,我们可以通过 getClass() 方法获取类名:

public class Example {
	public static void main(String[] args) {
		new OuterClass().method();
	}
}
 
interface Interface {
 
}
 
class OuterClass {
	public void method() {
		Interface i = new Interface() {
			// 匿名内部类
		};
 
		System.out.println(i.getClass());
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

运行结果:

如果你把这个类名占用了,它就会顺延:

public class Example {
	public static void main(String[] args) {
		new OuterClass().method();
	}
}
 
interface Interface {
 
}
 
class OuterClass$1 {
 
}
 
class OuterClass {
	public void method() {
		Interface i = new Interface() {
			// 匿名内部类
		};
 
		System.out.println(i.getClass());
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

运行结果:

2)在匿名内部类中访问外部类的成员

在及匿名内部类内可以直接访问到外部类的所有成员,包括私有的。

i.方法一:直接访问

abstract class Animal {
    abstract void foo();
}
 
class OuterClass {
	private String outerField = "outer class private field~";
 
	public void fun() {
		Animal cat = new Animal() {
            @Override
			public void foo() {
				System.out.println(outerField);// 直接访问
			}
		};
		
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

ii.方法二:通过外部类的 this 引用访问

abstract class Animal {
    abstract void foo();
}
 
class OuterClass {
	private String outerField = "outer class private field~";
 
	public void fun() {
		Animal cat = new Animal() {
            @Override
			public void foo() {
				System.out.println(OuterClass.this.outerField);// 通过外部类的this引用访问
			}
		};
 
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这个方法可以在外部类的成员与匿名内部类的成员重名时使用。因为如果外部类成员与匿名内部类成员重名,在匿名内部类中直接使用这个成员的名字访问到的 是匿名内部类的成员(遵循就近原则),如果想要访问外部类的这个成员,就要使用上面提到的方法。

格式:

`外部类名.this.成员名`
1

示例:

public class Example {
	public static void main(String[] args) {
		new OuterClass().fun();
	}
}
 
abstract class Animal {
	abstract void foo();
}
 
class OuterClass {
	private int a = 100;
 
	public void fun() {
		Animal cat = new Animal() {
			private int a = 200;
 
			@Override
			public void foo() {
				System.out.println("Anonymous inner class's field: " + a);
				System.out.println("Outer class's field: " + OuterClass.this.a);
			}
		};
 
		cat.foo();
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

运行结果:

iii.注意

需要注意的是,如果匿名内部类定义在一个静态的局部中(例如静态代码块和静态方法)中,则在匿名内部类中就不能访问到外部类的所有非静态成员。

3)在外部类定义匿名内部类的块中访问匿名内部类的成员

由于匿名内部类的类名不是显式的,所以我们只能使用匿名内部类的父类或者其实现接口类型的引用来指向匿名内部类的对象,这就导致我们只能访问到匿名内部类的父类或者其实现接口中有的成员。对于匿名内部类中的所有特有成员,外界都是不能直接访问到的,因为我们没法将这个匿名内部类的引用向下转型,所以就没有办法访问子类(匿名内部类)的特有成员。

所以我们实际上不能直接访问到匿名内部类的成员,只有匿名内部类重写了父类或者接口的默认方法,或者匿名内部类实现了接口的抽象方法,这样就可以通过动态绑定实现调用匿名内部类的方法。但也仅限于调用方法,因为对于属性是静态绑定的,所以匿名内部类中的所有属性外界都是不能直接访问的(这与上面说的匿名内部类的所有特有成员外界都不能访问一致)。

interface Interface {
	int interfaceField = 100;
	void cry();
}
 
class OuterClass {
	public void fun() {
		Interface cat = new Interface() {
			@Override
			public void cry() {
				System.out.println("Meow~");
			}
			
			public String anonymousClassField = "anonymous class's specific field~";
			public void anonymousClassMethod() {
				System.out.println("anonymous class's specific method~");
			}
		};
 
		cat.cry(); // 可以直接访问匿名内部类重写的方法,通过动态绑定机制
 
		System.out.println(cat.interfaceField); // 可以访问接口中的字段,因为引用是接口类型的
 
		// cat.anonymousClassMethod(); // 没法访问匿名内部类的特有方法
		
		// System.out.println(cat.anonymousClassField); // 没法访问匿名内部类的特有属性
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

4)匿名内部类的构造器

匿名内部类没有类名,因此不能定义显式的构造器。编译器会产生一个以外部类引用类型为参数的构造器。这里我们可以查看具体的编译器产生的语句。

public class Test {
	public static void main(String[] args) {
		
	}
}
 
class Animal {}
 
class OuterClass {
	public void fun() {
		Animal cat = new Animal() {
			
		};
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

我们使用 javac 将这段代码编译,可以得到以下文件:

可以看到编译器会为匿名内部类命名,也会产生字节码文件 OuterClass$1.class,然后我们使用 javap 工具得到 OuterClass$1.class 的反编译代码:

class OuterClass$1 extends Animal {
  final OuterClass this$0;
  OuterClass$1(OuterClass);
}
1
2
3
4

可以看到第一个字段为外部类 OuterClass 的 this 引用,这是编译器添加的。然后就是匿名内部类的构造器了,也就是 OuterClass$1(OuterClass),这表示匿名内部类的构造器接收一个外部类的引用类型的参数,这个引用是用来初始化上面的字段的,也就是用来初始化外部类的 this 引用,所以我们可以在匿名内部类中直接使用外部类的所有成员方法和属性。

5)使用匿名内部类的实例访问其成员的语法

一般情况下,我们会这样使用匿名内部类的实例访问其成员:

abstract class Animal {
	abstract void cry();
}
 
class OuterClass {
	public void fun() {
		Animal cat = new Animal() {
			@Override
			void cry() {
				System.out.println("Meow~");
			}
		};
 
		cat.cry();// 一般调用方式
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

其实我们也可以使用以下方式访问其成员:

abstract class Animal {
	abstract void cry();
}
 
class OuterClass {
	public void fun() {
		new Animal() {
			@Override
			void cry() {
				System.out.println("Meow~");
			}
		}.cry();// 创建对象之后立马调用,
				// 这种方式适用于创建实例后只使用一次这个实例的情形
				// 这中情况,前面不能使用Animal类型的引用来接受了,
				// 因为这样就是相当于调用了一个cry方法
				// 返回值是void
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

也就是说这种方式只能访问一次匿名内部类的实例的成员,然后这个实例就不能访问到了,也就被垃圾回收机制回收了。

6)匿名内部类访问定义它的块中的局部变量

匿名内部类也可以访问定义它的方法中的局部变量和方法的参数,也可以访问代码块中的局部变量。与局部内部类的要求一样,需要这些局部变量是 final 修饰的,或者是隐式 final 的(也就是初始化之后没有被修改过)。

访问定义匿名内部类的方法中的局部变量:

interface Interface {
	void cry();
}
 
class OuterClass {
	public void fun() {
		final int finalLocalVar = 0;
		int neverChangedVar = 0;
		// neverChangedVar = 1;
		// 这里如果有这一个语句的话,后面就会报错,因为这样这个局部变量就不是隐式final的了
		Interface cat = new Interface() {
			@Override
			public void cry() {
				System.out.println(finalLocalVar);
				System.out.println(neverChangedVar);
			}
		};
 
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

访问定义匿名内部类的方法的参数:

interface Interface {
	void cry();
}
 
class OuterClass {
	public void fun(final int finalParameter, int neverChangedParameter) {
		// neverChangedParameter = 0;
		// 这里如果有这一句的话,也会报错,因为这样这个参数就不是隐式final的了
		Interface cat = new Interface() {
			@Override
			public void cry() {
				System.out.println(finalParameter);
				System.out.println(neverChangedParameter);
			}
		};
 
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

访问定义了匿名内部类的代码块中的局部变量:

interface Interface {
	void cry();
}
 
class OuterClass {
	{
		final int finalLocalVar = 0;
		int neverChangedVar = 0;
		// neverChangedVar = 1;
		// 这里如果有这一个语句的话,后面就会报错,因为这样这个局部变量就不是隐式final的了
		Interface cat = new Interface() {
			@Override
			public void cry() {
				System.out.println(finalLocalVar);
				System.out.println(neverChangedVar);
			}
		};
 
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

3、补充

  1. 匿名内部类通常用于创建一个类的实例,而这个类只需要使用一次。
  2. 在匿名内部类中可以访问外部类的所有成员(包括私有的)。但是在外部类定义匿名内部类的块中不能直接访问到匿名内部类的成员。
  3. 匿名内部类可以继承一个类或实现一个接口。不能实现对多个接口实现。

4、匿名内部类的最佳实践

1)直接作为参数传递

直接将匿名内部类的实例作为参数传递,使代码更加简介明了。

public class Example {
	public static void animalCry(Cry c) {
		c.cry();
	}
 
	public static void main(String[] args) {
		animalCry(new Cry() { // 直接创建一个匿名内部类的实例,同时作为参数传给这个方法
			@Override
			public void cry() {
				System.out.println("Meow~");
			}
		});
	}
}
 
interface Cry {
	void cry();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 3.代理

# 一、什么是代理

        Java代理(Proxy)是Java编程语言 (opens new window)中一个非常重要的概念,它主要用于在不修改原有目标类代码的前提下,增加或修改目标类的行为。代理模式属于设计模式中的结构型模式,它通过引入一个代理类来间接访问目标对象,从而实现增强原有对象功能的目的。

        代理本质上是一种设计模式 (opens new window)。经常应用于Java训练当中。

例:以银行转账系统为例(包含验证、转账、服务三个方法)

①不使用代理

        如果正常写方法,不做任何强调,可能会将三个方法写到一起,混写成一个大的方法。如下图所示。

        直接混写成一个大的方法,其实并不好,因为完成验证、完成转账、和事后服务、都是及其复杂的流程,如果都写在一起的话,会使代码功能量太大。所以应该想方法降低功能量。

②使用代理

        参照上文,为了降低代码的功能量,很多人的思路为将每个功能单独写一个方法,此时每个方法都能完成单独的功能,不会造成大量的冗余,是可行的。

        但是还是没有考虑到扩容的问题。例如最开始只有银行转账,但现在要求开放对支付宝,微信的转账,即对其他公司开放转账的接口。以支付宝为例,将验证和服务交给支付宝,而核心功能即转账功能由银行负责,即代理模式。

        不允许其他人接触核心业务,但可以将核心业务交给别人去代理,用户只能访问代理的部门、无法接触到核心业务。(可以理解为买车,大多数人买车只能去4s店去购买,而不能去工厂直接购买。)

# 二、代理模式的好处

①防止用户直接访问核心方法,带来一系列不必要的危机。

②能够对核心方法进行功能的增强

# 三、java代理

        java代理分为静态代理(基于jdk实现的静态代理)和动态代理(基于jdk实现的动态代理、基于CGLB实现的动态代理)。

# 四、静态代理

# 1、什么是静态代理(定义、流程)

        静态代理是指在程序运行前就已经存在代理类的字节码文件,代理类和目标类的关系在运行前就已确定,且代理类通常是手工编写的。

        其模式为由核心类(目标类)生成核心对象(目标对象),由代理类生成代理对象,在由代理对象代理核心对象,而核心类和代理类都会实现同一个接口。

        接口起到了一个沟通(通知)的作用,即通知代理类所要代理的核心是什么。

# 2、静态代理的使用

以银行转账系统为例首先实现一个接口(ZhuanZhang)并定义一个核心方法zhuanzhang(),在创建一个YinHang类(核心类)去继承该接口,并实现核心方法。在创建一个ZhiFuBao类(代理类)继承接口并实现核心方法(先实现但不填写内容)。

        在代理类中,首先定义好一个被代理的类(核心类即YinHang类),然后在核心方法中调用该对象的zhuanzhang()方法即核心类的核心方法。然后实现功能的增强添加一个身份验证、金额验证和事后服务功能。最后在创建一个Test类本身无任何实际意义仅用来测试。代码如下。

接口:

public interface ZhuanZhang {
    public void zhuanzhang(String A,String B,Double money);
 
}
1
2
3
4

核心类(YinHang):

public class YinHang implements ZhuanZhang{
    public void zhuanzhang(String A, String B, Double money) {
        System.out.println(A+"给"+B+"转账了"+money);
    }
}
1
2
3
4
5

代理类(ZhiFuBao):

public class ZhiFuBao implements ZhuanZhang{
    private  YinHang yinHang =new YinHang();
    private void  yanzheng(String A,String B,Double money){
        System.out.println("对A进行了身份验证");
        System.out.println("对B进行了身份验证");
        System.out.println("对转账金额进行了身份验证");
 
    }
    private void fuwu(){
        System.out.println("转账完成后进行服务");
    }
    @Override
    public void zhuanzhang(String A, String B, Double money) {
       yanzheng(A,B,money);
       yinHang.zhuanzhang(A,B,money);
        fuwu();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Test类:

public class Test {
    public static void main(String[] args) {
        ZhiFuBao zhiFuBao=new ZhiFuBao();
        zhiFuBao.zhuanzhang("张三","李四",100.00);
    }
}
1
2
3
4
5
6

结果:

流程图:

        首先main()方法入栈,创建zhifubao对象,其中包括转账的方法、验证和服务方法。还会创建yinhang对象。该对象中也有转账方法。首先是zhifubao的转账方法入栈,然后调用yinhang的转账方法在这个过程中也完成了业务的增强。

# 3、静态代理的优点

①代理对象和目标对象在编译时就确定了关系,所以这种代理方式的安全性较高。

②代理类可以拥有额外的功能,比如可以在调用目标方法前后增加一些额外的操作。

# 4、静态代理的缺点

①用一个代理类代理多个目标类是很难实现的,工作量巨大。

②一旦目标接口增加方法,代理类也需要修改,不符合开闭原则(对扩展开放,对修改关闭)

举例:衣服工厂和鞋子工厂

        首先定义一个Clothes接口和一个核心方法ByClothes,在创建一个核心类ClothesFactory并实现核心方法ByClothes,接着创建代理类XSD,实现核心方法,首先第一步代理目标类,创建ClothesFactory对象,然后实现代理类的核心方法,调用核心类对象的核心方法。然后添加yige服务方法。在创建一个Test类测试。

        接着定义一个Shoes接口和一个核心方法ByShoes,在创建一个核心类ShoeFactory并实现核心方法Byshoes。接着在代理类继承Shoes接口并 在代理类当中创建ShoeFactory对象,并实现Shoes接口当中的核心方法。

        此时我们发现每增加一个目标类,就需要创建新的接口并在代理类当中不断进行扩充。每个代理对象代理的目标太多,模式图如下。

# 4.静态代理到动态代理

在软件开发中,设计模式是解决特定问题的一种通用方法,它帮助我们以更高效、更灵活的方式构建复杂的系统。代理模式(Proxy Pattern)作为结构型设计模式之一,广泛应用于各种场景。其核心思想是通过引入代理对象,为目标对象的访问提供一种间接方式,这种间接性不仅提升了代码的扩展性和维护性,还为功能的增强和安全控制提供了便利。

在实际应用中,代理模式可以分为静态代理 (opens new window)和动态代理。静态代理在编译时就确定了代理类的结构,而动态代理则通过运行时动态生成代理类,灵活应对多样化的需求。无论是为方法增加日志记录,性能监控,还是实现安全控制,代理模式都能够提供强大的支持。

本文将介绍代理模式的基本概念、实现方法及其原理,通过清晰的代码示例分别展示静态代理和动态代理的实现,解析两者的应用场景与优缺点,并结合动态代理的实现原理,帮助读者更好地理解和应用代理模式。

# 一、代理模式

代理模式是常用的java设计模式,他的特征是代理类与委托类有同样的接口,**代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。**代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。简单的说就是,我们在访问实际对象时,是通过代理对象来访问的,代理模式就是在访问实际对象时引入一定程度的间接性,因为这种间接性,可以附加多种用途。

# 二、静态代理

# (一)静态代理

静态代理:由程序员创建或特定工具自动生成源代码,也就是在编译时就已经将接口、被代理类、代理类等确定下来。在程序运行之前,代理类的.class文件就已经生成。

# (二)静态代理简单实现

根据上面代理模式的类图,来写一个简单的静态代理的例子:假如一个班的同学要向老师交班费,但是都是通过班长把自己的钱转交给老师。这里,班长代理学生上交班费,班长就是学生的代理。

  • 1.确定创建接口具体行为

首先,我们创建一个Person接口。这个接口就是学生(被代理类),和班长(代理类)的公共接口,他们都有上交班费的行为。这样,学生上交班费就可以让班长来代理执行。

/**
 * 创建Person接口
 */
public interface Person {
    //上交班费
    void giveMoney();
}

1
2
3
4
5
6
7
8
  • 2.被代理对象实现接口,完成具体的业务逻辑

Student类实现Person接口。Student可以具体实施上交班费的动作:

public class Student implements Person {
    private String name;
    public Student(String name) {
        this.name = name;
    }
    
    @Override
    public void giveMoney() {
       System.out.println(name + "上交班费50元");
    }
}
1
2
3
4
5
6
7
8
9
10
11
  • 3.代理类实现接口,完成委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。

StudentsProxy类,这个类也实现了Person接口,但是还另外持有一个学生类对象。由于实现了Peson接口,同时持有一个学生对象,那么他可以代理学生类对象执行上交班费(执行giveMoney()方法)行为。

/**
 * 学生代理类,也实现了Person接口,保存一个学生实体,这样既可以代理学生产生行为
 * @author Gonjan
 *
 */
public class StudentsProxy implements Person{
    //被代理的学生
    Student stu;
    
    public StudentsProxy(Person stu) {
        // 只代理学生对象
        if(stu.getClass() == Student.class) {
            this.stu = (Student)stu;
        }
    }
    
    //代理上交班费,调用被代理学生的上交班费行为
    public void giveMoney() {
        stu.giveMoney();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  • 4.客户端使用操作与分析
public class StaticProxyTest {
    public static void main(String[] args) {
        //被代理的学生张三,他的班费上交有代理对象monitor(班长)完成
        Person zhangsan = new Student("张三");
        
        //生成代理对象,并将张三传给代理对象
        Person monitor = new StudentsProxy(zhangsan);
        
        //班长代理上交班费
        monitor.giveMoney();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

这里并没有直接通过张三(被代理对象)来执行上交班费的行为,而是通过班长(代理对象)来代理执行了,这就是代理模式。

代理模式最主要的就是有一个公共接口(Person),一个具体的类(Student),一个代理类(StudentsProxy),代理类持有具体类的实例,代为执行具体类实例方法。上面说到,代理模式就是在访问实际对象时引入一定程度的间接性,因为这种间接性,可以附加多种用途。这里的间接性就是指不直接调用实际对象的方法,那么我们在代理过程中就可以加上一些其他用途。就这个例子来说,加入班长在帮张三上交班费之前想要先反映一下张三最近学习有很大进步,通过代理模式很轻松就能办到:

public class StudentsProxy implements Person{
    //被代理的学生
    Student stu;
    
    public StudentsProxy(Person stu) {
        // 只代理学生对象
        if(stu.getClass() == Student.class) {
            this.stu = (Student)stu;
        }
    }
    
    //代理上交班费,调用被代理学生的上交班费行为
    public void giveMoney() {
        System.out.println("张三最近学习有进步!");
        stu.giveMoney();
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

只需要在代理类中帮张三上交班费之前,执行其他操作就可以了。这种操作,也是使用代理模式的一个很大的优点。最直白的就是在Spring中的面向切面编程(AOP),我们能在一个切点之前执行一些操作,在一个切点之后执行一些操作,这个切点就是一个个方法。这些方法所在类肯定就是被代理了,在代理过程中切入了一些其他操作。其实也是切面思想的主要思路,这个后面会出一篇博客,并且举例怎么使用,想法是:对controller层出入参进行检验和必要的操作等,Spring源码里面也有许多这样的例子,后期有时间整理。

# 三、动态代理

# (一)动态代理

代理类在程序运行时创建的代理方式被成为动态代理

我们上面静态代理的例子中,代理类(studentProxy)是自己定义好的,在程序运行之前就已经编译完成。然而动态代理,代理类并不是在Java代码中定义的,而是在运行时根据我们在Java代码中的“指示”动态生成的。相比于静态代理, 动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。 比如说,想要在每个代理的方法前都加上一个处理方法:

    public void giveMoney() {
        //调用被代理方法前加入处理方法
        beforeMethod();
        stu.giveMoney();
    }
1
2
3
4
5

这里只有一个giveMoney方法,就写一次beforeMethod方法,但是如果除了giveMonney还有很多其他的方法,那就需要写很多次beforeMethod方法,麻烦。所以建议使用动态代理实现。

# (二)动态代理简单实现

在java的java.lang.reflect包下提供了一个Proxy类和一个InvocationHandler接口通过这个类和这个接口可以生成JDK动态代理类和动态代理对象

  • 1.确定创建接口具体行为

首先,我们创建一个Person接口。这个接口就是学生(被代理类),和班长(代理类)的公共接口,他们都有上交班费的行为。这样,学生上交班费就可以让班长来代理执行。

/**
 * 创建Person接口
 */
public interface Person {
    //上交班费
    void giveMoney();
}
1
2
3
4
5
6
7
  • 2.被代理对象实现接口,完成具体的业务逻辑(此处增加一些方法用于检测后面使用动态代理用于区分)

Student类实现Person接口。

public class Student implements Person {
    private String name;
    public Student(String name) {
        this.name = name;
    }
    
    @Override
    public void giveMoney() {
        try {
            //假设数钱花了一秒时间
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
       System.out.println(name + "上交班费50元");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

再定义一个检测方法执行时间的工具类,在任何方法执行前先调用start方法,执行后调用finsh方法,就可以计算出该方法的运行时间,这也是一个最简单的方法执行时间检测工具。

public class MonitorUtil {
    
    private static ThreadLocal<Long> tl = new ThreadLocal<>();
    
    public static void start() {
        tl.set(System.currentTimeMillis());
    }
    
    //结束时打印耗时
    public static void finish(String methodName) {
        long finishTime = System.currentTimeMillis();
        System.out.println(methodName + "方法耗时" + (finishTime - tl.get()) + "ms");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 3.代理类实现接口,完成委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。

创建StuInvocationHandler类,实现InvocationHandler接口,这个类中持有一个被代理对象的实例target。InvocationHandler中有一个invoke方法,所有执行代理对象的方法都会被替换成执行invoke方法。在invoke方法中执行被代理对象target的相应方法。在代理过程中,我们在真正执行被代理对象的方法前加入自己其他处理。这也是Spring中的AOP实现的主要原理,这里还涉及到一个很重要的关于java反射方面的基础知识

public class StuInvocationHandler<T> implements InvocationHandler {
    //invocationHandler持有的被代理对象
    T target;
    
    public StuInvocationHandler(T target) {
       this.target = target;
    }
    
    /**
     * proxy:代表动态代理对象
     * method:代表正在执行的方法
     * args:代表调用目标方法时传入的实参
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("代理执行" +method.getName() + "方法");  
        //代理过程中插入监测方法,计算该方法耗时
        MonitorUtil.start();
        Object result = method.invoke(target, args);
        MonitorUtil.finish(method.getName());
        return result;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  • 4.客户端使用操作与分析
public class ProxyTest {
    public static void main(String[] args) {
        
        //创建一个实例对象,这个对象是被代理的对象
        Person zhangsan = new Student("张三");
        
        //创建一个与代理对象相关联的InvocationHandler
        InvocationHandler stuHandler = new StuInvocationHandler<Person>(zhangsan);
        
        //创建一个代理对象stuProxy来代理zhangsan,代理对象的每个执行方法都会替换执行Invocation中的invoke方法
        Person stuProxy = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class<?>[]{Person.class}, stuHandler)//代理执行上交班费的方法
        stuProxy.giveMoney();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

创建了一个需要被代理的学生张三,将zhangsan对象传给了stuHandler中,我们在创建代理对象stuProxy时,将stuHandler作为参数了的,上面也有说到所有执行代理对象的方法都会被替换成执行invoke方法,也就是说,最后执行的是StuInvocationHandler中的invoke方法。

动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。是因为所有被代理执行的方法,都是通过在InvocationHandler中的invoke方法调用的,所以我们只要在invoke方法中统一处理,就可以对所有被代理的方法进行相同的操作了。例如,这里的方法计时,所有的被代理对象执行的方法都会被计时,然而我只做了很少的代码量。

动态代理的过程,代理对象和被代理对象的关系不像静态代理那样一目了然,清晰明了。因为动态代理的过程中,我们并没有实际看到代理类,也没有很清晰地的看到代理类的具体样子,而且动态代理中被代理对象和代理对象是通过InvocationHandler来完成的代理过程的,其中具体是怎样操作的,为什么代理对象执行的方法都会通过InvocationHandler中的invoke方法来执行。带着这些问题,我们就需要对java动态代理的源码进行简要的分析,弄清楚其中缘由。

# 四、动态代理原理分析

我们利用Proxy类的newProxyInstance方法创建了一个动态代理对象,查看该方法的源码,发现它只是封装了创建动态代理类的步骤

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,
                          InvocationHandler h) throws IllegalArgumentException {
        Objects.requireNonNull(h);
 
        final Class<?>[] intfs = interfaces.clone();
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }
 
        /*
         * Look up or generate the designated proxy class.
         */
        Class<?> cl = getProxyClass0(loader, intfs);
 
        /*
         * Invoke its constructor with the designated invocation handler.
         */
        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }
 
            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            return cons.newInstance(new Object[]{h});
        } catch (IllegalAccessException|InstantiationException e) {
            throw new InternalError(e.toString(), e);
        } catch (InvocationTargetException e) {
            Throwable t = e.getCause();
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new InternalError(t.toString(), t);
            }
        } catch (NoSuchMethodException e) {
            throw new InternalError(e.toString(), e);
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

重点是这四处位置:

final Class<?>[] intfs = interfaces.clone();
Class<?> cl = getProxyClass0(loader, intfs);
final Constructor<?> cons = cl.getConstructor(constructorParams);
return cons.newInstance(new Object[]{h});
1
2
3
4

最应该关注的是 Class<?> cl = getProxyClass0(loader, intfs);这句,这里产生了代理类,后面代码中的构造器也是通过这里产生的类来获得,可以看出,这个类的产生就是整个动态代理的关键,由于是动态生成的类文件,我这里不具体进入分析如何产生的这个类文件,只需要知道这个类文件时缓存在java虚拟机中的,我们可以通过下面的方法将其打印到文件里面,一睹真容:

byte[] classFile = ProxyGenerator.generateProxyClass("$Proxy0",Student.class.getInterfaces());
String path = "G:/javacode/javase/Test/bin/proxy/StuProxy.class";
try(FileOutputStream fos = new FileOutputStream(path)) {
    fos.write(classFile);
    fos.flush();
    System.out.println("代理类class文件写入成功");
} catch (Exception e) {
    System.out.println("写文件错误");
}
1
2
3
4
5
6
7
8
9

对这个class文件进行反编译,我们看看jdk为我们生成了什么样的内容:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import proxy.Person;
 
public final class $Proxy0 extends Proxy implements Person {
  private static Method m1;
  private static Method m2;
  private static Method m3;
  private static Method m0;
  
  /**
  *注意这里是生成代理类的构造方法,方法参数为InvocationHandler类型,看到这,是不是就有点明白
  *为何代理对象调用方法都是执行InvocationHandler中的invoke方法,而InvocationHandler又持有一个
  *被代理对象的实例,不禁会想难道是....? 没错,就是你想的那样。
  *
  *super(paramInvocationHandler),是调用父类Proxy的构造方法。
  *父类持有:protected InvocationHandler h;
  *Proxy构造方法:
  *    protected Proxy(InvocationHandler h) {
  *         Objects.requireNonNull(h);
  *         this.h = h;
  *     }
  *
  */
  public $Proxy0(InvocationHandler paramInvocationHandler)
    throws 
  {
    super(paramInvocationHandler);
  }
  
  //这个静态块本来是在最后的,我把它拿到前面来,方便描述
   static
  {
    try
    {
      //看看这儿静态块儿里面有什么,是不是找到了giveMoney方法。请记住giveMoney通过反射得到的名字m3,其他的先不管
      m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
      m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
      m3 = Class.forName("proxy.Person").getMethod("giveMoney", new Class[0]);
      m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
      return;
    }
    catch (NoSuchMethodException localNoSuchMethodException)
    {
      throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
    }
    catch (ClassNotFoundException localClassNotFoundException)
    {
      throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
    }
  }
 
  /**
  * 
  *这里调用代理对象的giveMoney方法,直接就调用了InvocationHandler中的invoke方法,并把m3传了进去。
  *this.h.invoke(this, m3, null);这里简单,明了。
  *来,再想想,代理对象持有一个InvocationHandler对象,InvocationHandler对象持有一个被代理的对象,
  *再联系到InvacationHandler中的invoke方法。嗯,就是这样。
  */
  public final void giveMoney()
    throws 
  {
    try
    {
      this.h.invoke(this, m3, null);
      return;
    }
    catch (Error|RuntimeException localError)
    {
      throw localError;
    }
    catch (Throwable localThrowable)
    {
      throw new UndeclaredThrowableException(localThrowable);
    }
  }
 
  //注意,这里为了节省篇幅,省去了toString,hashCode、equals方法的内容。原理和giveMoney方法一毛一样。
 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

jdk为我们的生成了一个叫**$Proxy0(这个名字后面的0是编号,有多个代理类会一次递增)的代理类,这个类文件时放在内存中的,我们在创建代理对象时,就是通过反射获得这个类的构造方法,然后创建的代理实例**。通过对这个生成的代理类源码的查看,我们很容易能看出,动态代理实现的具体过程。

我们可以对InvocationHandler看做一个中介类,中介类持有一个被代理对象,在invoke方法中调用了被代理对象的相应方法。通过聚合方式持有被代理对象的引用,把外部对invoke的调用最终都转为对被代理对象的调用。

代理类调用自己方法时,通过自身持有的中介类对象来调用中介类对象的invoke方法,从而达到代理执行被代理对象的方法。也就是说,动态代理通过中介类实现了具体的代理功能。

总结:生成的代理类:$Proxy0 extends Proxy implements Person,我们看到代理类继承了Proxy类,所以也就决定了java动态代理只能对接口进行代理,Java的继承机制注定了这些动态代理类们无法实现对class的动态代理。上面的动态代理的例子,其实就是AOP的一个简单实现了,在目标对象的方法执行之前和执行之后进行了处理,对方法耗时统计。Spring的AOP实现其实也是用了Proxy和InvocationHandler这两个东西的

# 五、InvocationHandler接口和Proxy类详解

InvocationHandler接口是proxy代理实例的调用处理程序实现的一个接口,每一个proxy代理实例都有一个关联的调用处理程序;在代理实例调用方法时,方法调用被编码分派到调用处理程序的invoke方法
看下官方文档对InvocationHandler接口的描述:

{@code InvocationHandler} is the interface implemented by
     the <i>invocation handler</i> of a proxy instance.
     <p>Each proxy instance has an associated invocation handler.
     When a method is invoked on a proxy instance, the method
     invocation is encoded and dispatched to the {@code invoke}
     method of its invocation handler.
1
2
3
4
5
6

当我们通过动态代理对象调用一个方法时候,这个方法的调用就会被转发到实现InvocationHandler接口类的invoke方法来调用,看如下invoke方法:

    /**
        * proxy:代理类代理的真实代理对象com.sun.proxy.$Proxy0
        * method:我们所要调用某个对象真实的方法的Method对象
        * args:指代代理对象方法传递的参数
        */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
1
2
3
4
5
6

Proxy类就是用来创建一个代理对象的类,它提供了很多方法,但是我们最常用的是newProxyInstance方法。

public static Object newProxyInstance(ClassLoader loader,   
                                                                                        Class<?>[] interfaces,  
                                                                                        InvocationHandler h)
1
2
3

这个方法的作用就是创建一个代理类对象,它接收三个参数,我们来看下几个参数的含义:

  • loader:一个classloader对象,定义了由哪个classloader对象对生成的代理类进行加载
  • interfaces:一个interface对象数组,表示我们将要给我们的代理对象提供一组什么样的接口,如果我们提供了这样一个接口对象数组,那么也就是声明了代理类实现了这些接口,代理类就可以调用接口中声明的所有方法。
  • h:一个InvocationHandler对象,表示的是当动态代理对象调用方法的时候会关联到哪一个InvocationHandler对象上,并最终由其调用。

# 六、JDK动态代理和CGLIB动态代理代码示例比较与总结

# (一)定义创建用户管理接口

/**
 * 用户管理接口
 */
public interface UserManager {
    //新增用户抽象方法
    void addUser(String userName, String password);
    //删除用户抽象方法
    void delUser(String userName);
}

1
2
3
4
5
6
7
8
9
10

# (二)用户管理实现类,实现用户管理接口(被代理的实现类)

/**
 * 用户管理实现类,实现用户管理接口(被代理的实现类)
 */
public class UserManagerImpl implements UserManager{
 
    //重写用户新增方法
    @Override
    public void addUser(String userName, String password) {
        System.out.println("调用了用户新增的方法!");
        System.out.println("传入参数:\nuserName = " + userName +", password = " + password);
    }
 
    //重写删除用户方法
    @Override
    public void delUser(String userName) {
        System.out.println("调用了删除的方法!");
        System.out.println("传入参数:\nuserName = "+userName);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# (三)采用JDK代理实现:JDK动态代理 (opens new window)实现InvocationHandler接口

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
 
/**
 * JDK动态代理实现InvocationHandler接口
 */
public class JdkProxy implements InvocationHandler {
 
    private Object targetObject;  //需要代理的目标对象
 
 
    //定义获取代理对象的方法(将目标对象传入进行代理)
    public Object getJDKProxy(Object targetObject){
        //为目标对象target赋值
        this.targetObject = targetObject;
        //JDK动态代理只能针对实现了接口的类进行代理,newProxyInstance 函数所需参数就可看出
        Object proxyObject = Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),targetObject.getClass().getInterfaces(),this);
        //返回代理对象
        return proxyObject;
    }
 
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("JDK动态代理,监听开始!");
        // 调用invoke方法,result存储该方法的返回值
        Object result = method.invoke(targetObject,args);
        System.out.println("JDK动态代理,监听结束!");
        return result;
    }
 
//    public static void main(String[] args) {
//        JdkProxy jdkProxy = new JdkProxy();  //实例化JDKProxy对象
//        UserManager user = (UserManager) jdkProxy.getJDKProxy(new UserManagerImpl());   //获取代理对象
//        user.addUser("admin","123456");
//    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# (四)采用CGLIB代理实现:需要导入asm版本包,实现MethodInterceptor接口

import com.proxy.UserManager;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
 
import java.lang.reflect.Method;
 
/**
 * Cglib动态代理:
 * (需要导入两个jar包,asm-5.0.3.jar,cglib-3.1.jar 版本可自行选择)
 */
 
//Cglib动态代理,实现MethodInterceptor接口
public class CglibProxy implements MethodInterceptor {
    private Object target;//需要代理的目标对象
 
    //重写拦截方法
    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("Cglib动态代理,监听开始!");
        Object result = method.invoke(target,args);//方法执行参数:target 目标对象 arr参数数组
        System.out.println("Cglib动态代理,监听结束!");
        return result;
    }
 
    //定义获取代理对象的方法
    public UserManager getCglibProxy(Object targetObject) {
        this.target = targetObject;//为目标对象target赋值
        Enhancer enhancer = new Enhancer();
        //设置父类,因为Cglib是针对指定的类生成一个子类,所以需要指定父类
        enhancer.setSuperclass(targetObject.getClass()); //UserManagerImpl
        enhancer.setCallback(this);//设置回调
        Object result = enhancer.create();//创建并返回代理对象
        return (UserManager) result;
    }
 
 
//    public static void main(String[] args) {
//        CglibProxy cglibProxy = new CglibProxy(); //实例化CglibProxy对象
//        UserManager user = cglibProxy.getCglibProxy(new UserManagerImpl());//获取代理对象
//        user.delUser("admin");
//    }
 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# (五)客户端调用测试与结果

import com.proxy.CglibProxy.CglibProxy;
import com.proxy.JDKProxy.JdkProxy;
 
public class ClientTest {
    public static void main(String[] args) {
 
        JdkProxy jdkProxy = new JdkProxy();  //实例化JDKProxy对象
        UserManager userJdk = (UserManager) jdkProxy.getJDKProxy(new UserManagerImpl());   //获取代理对象
        userJdk.addUser("admin","123456");
 
        CglibProxy cglibProxy = new CglibProxy(); //实例化CglibProxy对象
        UserManager userCglib = cglibProxy.getCglibProxy(new UserManagerImpl());//获取代理对象
        userCglib.delUser("admin");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

运行结果:

JDK动态代理,监听开始!
调用了用户新增的方法!
传入参数:
userName = admin, password = 123456
JDK动态代理,监听结束!
Cglib动态代理,监听开始!
调用了删除的方法!
传入参数:
userName = admin
Cglib动态代理,监听结束!
1
2
3
4
5
6
7
8
9
10

# (六)JDK和CGLIB动态代理总结

  • JDK动态代理只能对实现了接口的类生成代理,而不能针对类 ,使用的是 Java反射技术实现,生成类的过程比较高效
  • CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法 ,使用asm字节码框架实现,相关执行的过程比较高效,生成类的过程可以利用缓存弥补,因为是继承,所以该类或方法最好不要声明成final
  • JDK代理是不需要第三方库支持,只需要JDK环境就可以进行代理,使用条件:实现InvocationHandler + 使用Proxy.newProxyInstance产生代理对象 + 被代理的对象必须要实现接口
  • CGLib必须依赖于CGLib的类库,但是它需要类来实现任何接口代理的是指定的类生成一个子类,覆盖其中的方法,是一种继承但是针对接口编程的环境下推荐使用JDK的代理

# 七、总结

代理模式作为结构型设计模式中的重要一环,在软件开发中具有广泛的应用价值。它通过引入代理对象,为目标对象的访问提供了一种灵活且可扩展的解决方案。无论是静态代理还是动态代理,都展现了在不同场景下的强大优势:静态代理适合简单明确的功能增强,而动态代理则凭借其运行时生成的特性,能够应对复杂多变的需求。

在实际应用中,代理模式不仅可以用于功能增强,如日志记录、性能监控和安全控制,还在分布式系统中扮演着关键角色,例如远程代理和缓存代理。在实现过程中,动态代理的强大能力特别值得关注,其通过反射或字节码生成机制,为高效开发提供了更多可能性。

通过对代理模式的深入理解和灵活运用,开发者能够设计出更加优雅、高效且易于维护的代码架构。希望本文的介绍和示例代码,能帮助读者在工作中更好地理解和应用代理模式,为解决实际问题提供思路和借鉴。