框架基礎:深入理解Java注解類型(@Annotation)

注解的概念

注解的官方定義

首先看看官方對注解的描述:

An annotation is a form of metadata, that can be added to Java source code. Classes, methods, variables, parameters and packages may be annotated. Annotations have no direct effect on the operation of the code they annotate.

翻譯:

注解是一種能被添加到java代碼中的元數據,類、方法、變量、參數和包都可以用注解來修飾。注解對于它所修飾的代碼并沒有直接的影響。

通過官方描述得出以下結論:

注解是一種元數據形式。即注解是屬于java的一種數據類型,和類、接口、數組、枚舉類似。
注解用來修飾,類、方法、變量、參數、包。
注解不會對所修飾的代碼產生直接的影響。

注解的使用范圍

繼續看看官方對它的使用范圍的描述:

Annotations have a number of uses, among them:Information for the complier - Annotations can be used by the compiler to detect errors or suppress warnings.Compiler-time and deployment-time processing - Software tools can process annotation information to generate code, XML files, and so forth.Runtime processing - Some annotations are available to be examined at runtime.

翻譯:

注解又許多用法,其中有:為編譯器提供信息 - 注解能被編譯器檢測到錯誤或抑制警告。編譯時和部署時的處理 - 軟件工具能處理注解信息從而生成代碼,XML文件等等。運行時的處理 - 有些注解在運行時能被檢測到。

##2 如何自定義注解
基于上一節,已對注解有了一個基本的認識:注解其實就是一種標記,可以在程序代碼中的關鍵節點(類、方法、變量、參數、包)上打上這些標記,然后程序在編譯時或運行時可以檢測到這些標記從而執行一些特殊操作。因此可以得出自定義注解使用的基本流程:

第一步,定義注解——相當于定義標記;
第二步,配置注解——把標記打在需要用到的程序代碼中;
第三步,解析注解——在編譯期或運行時檢測到標記,并進行特殊操作。

基本語法

注解類型的聲明部分:

注解在Java中,與類、接口、枚舉類似,因此其聲明語法基本一致,只是所使用的關鍵字有所不同@interface。在底層實現上,所有定義的注解都會自動繼承java.lang.annotation.Annotation接口。

public @interface CherryAnnotation {
}

注解類型的實現部分:

根據我們在自定義類的經驗,在類的實現部分無非就是書寫構造、屬性或方法。但是,在自定義注解中,其實現部分只能定義一個東西:注解類型元素(annotation type element)。咱們來看看其語法:

public @interface CherryAnnotation {public String name();int age() default 18;int[] array();
}

定義注解類型元素時需要注意如下幾點:

  1. 訪問修飾符必須為public,不寫默認為public;

  2. 該元素的類型只能是基本數據類型、String、Class、枚舉類型、注解類型(體現了注解的嵌套效果)以及上述類型的一位數組;

  3. 該元素的名稱一般定義為名詞,如果注解中只有一個元素,請把名字起為value(后面使用會帶來便利操作);

  4. ()不是定義方法參數的地方,也不能在括號中定義任何參數,僅僅只是一個特殊的語法;

  5. default代表默認值,值必須和第2點定義的類型一致;

  6. 如果沒有默認值,代表后續使用注解時必須給該類型元素賦值。

常用的元注解

一個最最基本的注解定義就只包括了上面的兩部分內容:1、注解的名字;2、注解包含的類型元素。但是,我們在使用JDK自帶注解的時候發現,有些注解只能寫在方法上面(比如@Override);有些卻可以寫在類的上面(比如@Deprecated)。當然除此以外還有很多細節性的定義,那么這些定義該如何做呢?接下來就該元注解出場了!
元注解:專門修飾注解的注解。它們都是為了更好的設計自定義注解的細節而專門設計的。我們為大家一個個來做介紹。

@Target

@Target注解,是專門用來限定某個自定義注解能夠被應用在哪些Java元素上面的。它使用一個枚舉類型定義如下:

public enum ElementType {/** 類,接口(包括注解類型)或枚舉的聲明 */TYPE,/** 屬性的聲明 */FIELD,/** 方法的聲明 */METHOD,/** 方法形式參數聲明 */PARAMETER,/** 構造方法的聲明 */CONSTRUCTOR,/** 局部變量聲明 */LOCAL_VARIABLE,/** 注解類型聲明 */ANNOTATION_TYPE,/** 包的聲明 */PACKAGE
}

?

//@CherryAnnotation被限定只能使用在類、接口或方法上面
@Target(value = {ElementType.TYPE,ElementType.METHOD})
public @interface CherryAnnotation {String name();int age() default 18;int[] array();
}

@Retention

@Retention注解,翻譯為持久力、保持力。即用來修飾自定義注解的生命力。
注解的生命周期有三個階段:1、Java源文件階段;2、編譯到class文件階段;3、運行期階段。同樣使用了RetentionPolicy枚舉類型定義了三個階段:

public enum RetentionPolicy {/*** Annotations are to be discarded by the compiler.* (注解將被編譯器忽略掉)*/SOURCE,/*** Annotations are to be recorded in the class file by the compiler* but need not be retained by the VM at run time.  This is the default* behavior.* (注解將被編譯器記錄在class文件中,但在運行時不會被虛擬機保留,這是一個默認的行為)*/CLASS,/*** Annotations are to be recorded in the class file by the compiler and* retained by the VM at run time, so they may be read reflectively.* (注解將被編譯器記錄在class文件中,而且在運行時會被虛擬機保留,因此它們能通過反射被讀取到)* @see java.lang.reflect.AnnotatedElement*/RUNTIME
}

我們再詳解一下:

  1. 如果一個注解被定義為RetentionPolicy.SOURCE,則它將被限定在Java源文件中,那么這個注解即不會參與編譯也不會在運行期起任何作用,這個注解就和一個注釋是一樣的效果,只能被閱讀Java文件的人看到;

  2. 如果一個注解被定義為RetentionPolicy.CLASS,則它將被編譯到Class文件中,那么編譯器可以在編譯時根據注解做一些處理動作,但是運行時JVM(Java虛擬機)會忽略它,我們在運行期也不能讀取到;

  3. 如果一個注解被定義為RetentionPolicy.RUNTIME,那么這個注解可以在運行期的加載階段被加載到Class對象中。那么在程序運行階段,我們可以通過反射得到這個注解,并通過判斷是否有這個注解或這個注解中屬性的值,從而執行不同的程序代碼段。我們實際開發中的自定義注解幾乎都是使用的RetentionPolicy.RUNTIME;

自定義注解

在具體的Java類上使用注解

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
@Documented
public @interface CherryAnnotation {String name();int age() default 18;int[] score();
}

?

public class Student {@CherryAnnotation(name = "cherry-peng",age = 23,score = {99,66,77})public void study(int times){for(int i = 0; i < times; i++){System.out.println("Good Good Study, Day Day Up!");}}
}

簡單分析下:

  1. CherryAnnotation的@Target定義為ElementType.METHOD,那么它書寫的位置應該在方法定義的上方,即:public void study(int times)之上;

  2. 由于我們在CherryAnnotation中定義的有注解類型元素,而且有些元素是沒有默認值的,這要求我們在使用的時候必須在標記名后面打上(),并且在()內以“元素名=元素值“的形式挨個填上所有沒有默認值的注解類型元素(有默認值的也可以填上重新賦值),中間用“,”號分割;

注解與反射機制

為了運行時能準確獲取到注解的相關信息,Java在java.lang.reflect 反射包下新增了AnnotatedElement接口,它主要用于表示目前正在 VM 中運行的程序中已使用注解的元素,通過該接口提供的方法可以利用反射技術地讀取注解的信息,如反射包的Constructor類、Field類、Method類、Package類和Class類都實現了AnnotatedElement接口,它簡要含義如下:

Class:類的Class對象定義  ?
Constructor:代表類的構造器定義  ?
Field:代表類的成員變量定義?
Method:代表類的方法定義  ?
Package:代表類的包定義

下面是AnnotatedElement中相關的API方法,以上5個類都實現以下的方法

?返回值?方法名稱?說明
?<A extends Annotation>?getAnnotation(Class<A> annotationClass)?該元素如果存在指定類型的注解,則返回這些注解,否則返回 null。
?Annotation[]?getAnnotations()?返回此元素上存在的所有注解,包括從父類繼承的
?boolean?isAnnotationPresent(Class<? extends Annotation> annotationClass)?如果指定類型的注解存在于此元素上,則返回 true,否則返回 false。
?Annotation[]?getDeclaredAnnotations()?返回直接存在于此元素上的所有注解,注意,不包括父類的注解,調用者可以隨意修改返回的數組;這不會對其他調用者返回的數組產生任何影響,沒有則返回長度為0的數組

?

簡單案例演示如下:

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DocumentA {
}

?

package com.zejian.annotationdemo;import java.lang.annotation.Annotation;
import java.util.Arrays;@DocumentA
class A{}//繼承了A類
@DocumentB
public class DocumentDemo extends A{public static void main(String... args){Class<?> clazz = DocumentDemo.class;//根據指定注解類型獲取該注解DocumentA documentA=clazz.getAnnotation(DocumentA.class);System.out.println("A:"+documentA);//獲取該元素上的所有注解,包含從父類繼承Annotation[] an= clazz.getAnnotations();System.out.println("an:"+ Arrays.toString(an));//獲取該元素上的所有注解,但不包含繼承!Annotation[] an2=clazz.getDeclaredAnnotations();System.out.println("an2:"+ Arrays.toString(an2));//判斷注解DocumentA是否在該元素上boolean b=clazz.isAnnotationPresent(DocumentA.class);System.out.println("b:"+b);}
}

執行結果:

A:@com.zejian.annotationdemo.DocumentA()
an:[@com.zejian.annotationdemo.DocumentA(), @com.zejian.annotationdemo.DocumentB()]
an2:@com.zejian.annotationdemo.DocumentB()
b:true

通過反射獲取上面我們自定義注解

public class TestAnnotation {public static void main(String[] args){try {//獲取Student的Class對象Class stuClass = Class.forName("pojos.Student");//說明一下,這里形參不能寫成Integer.class,應寫為int.classMethod stuMethod = stuClass.getMethod("study",int.class);if(stuMethod.isAnnotationPresent(CherryAnnotation.class)){System.out.println("Student類上配置了CherryAnnotation注解!");//獲取該元素上指定類型的注解CherryAnnotation cherryAnnotation = stuMethod.getAnnotation(CherryAnnotation.class);System.out.println("name: " + cherryAnnotation.name() + ", age: " + cherryAnnotation.age()+ ", score: " + cherryAnnotation.score()[0]);}else{System.out.println("Student類上沒有配置CherryAnnotation注解!");}} catch (ClassNotFoundException e) {e.printStackTrace();} catch (NoSuchMethodException e) {e.printStackTrace();}}
}

運行時注解處理器

了解完注解與反射的相關API后,現在通過一個實例(該例子是博主改編自《Tinking in Java》)來演示利用運行時注解來組裝數據庫SQL的構建語句的過程

/*** Created by ChenHao on 2019/6/14.* 表注解*/
@Target(ElementType.TYPE)//只能應用于類上
@Retention(RetentionPolicy.RUNTIME)//保存到運行時
public @interface DBTable {String name() default "";
}/*** Created by ChenHao on 2019/6/14.* 注解Integer類型的字段*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {//該字段對應數據庫表列名String name() default "";//嵌套注解Constraints constraint() default @Constraints;
}/*** Created by ChenHao on 2019/6/14.* 注解String類型的字段*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {//對應數據庫表的列名String name() default "";//列類型分配的長度,如varchar(30)的30int value() default 0;Constraints constraint() default @Constraints;
}/*** Created by ChenHao on 2019/6/14.* 約束注解*/@Target(ElementType.FIELD)//只能應用在字段上
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {//判斷是否作為主鍵約束boolean primaryKey() default false;//判斷是否允許為nullboolean allowNull() default false;//判斷是否唯一boolean unique() default false;
}/*** Created by ChenHao on 2019/6/14.* 數據庫表Member對應實例類bean*/
@DBTable(name = "MEMBER")
public class Member {//主鍵ID@SQLString(name = "ID",value = 50, constraint = @Constraints(primaryKey = true))private String id;@SQLString(name = "NAME" , value = 30)private String name;@SQLInteger(name = "AGE")private int age;@SQLString(name = "DESCRIPTION" ,value = 150 , constraint = @Constraints(allowNull = true))private String description;//個人描述//省略set get.....
}

上述定義4個注解,分別是@DBTable(用于類上)、@Constraints(用于字段上)、 @SQLString(用于字段上)、@SQLString(用于字段上)并在Member類中使用這些注解,這些注解的作用的是用于幫助注解處理器生成創建數據庫表MEMBER的構建語句,在這里有點需要注意的是,我們使用了嵌套注解@Constraints,該注解主要用于判斷字段是否為null或者字段是否唯一。必須清楚認識到上述提供的注解生命周期必須為@Retention(RetentionPolicy.RUNTIME),即運行時,這樣才可以使用反射機制獲取其信息。有了上述注解和使用,剩余的就是編寫上述的注解處理器了,前面我們聊了很多注解,其處理器要么是Java自身已提供、要么是框架已提供的,我們自己都沒有涉及到注解處理器的編寫,但上述定義處理SQL的注解,其處理器必須由我們自己編寫了,如下

package com.chenHao.annotationdemo;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;/*** Created by chenhao on 2019/6/14.* 運行時注解處理器,構造表創建語句*/
public class TableCreator {public static String createTableSql(String className) throws ClassNotFoundException {Class<?> cl = Class.forName(className);DBTable dbTable = cl.getAnnotation(DBTable.class);//如果沒有表注解,直接返回if(dbTable == null) {System.out.println("No DBTable annotations in class " + className);return null;}String tableName = dbTable.name();// If the name is empty, use the Class name:if(tableName.length() < 1)tableName = cl.getName().toUpperCase();List<String> columnDefs = new ArrayList<String>();//通過Class類API獲取到所有成員字段for(Field field : cl.getDeclaredFields()) {String columnName = null;//獲取字段上的注解Annotation[] anns = field.getDeclaredAnnotations();if(anns.length < 1)continue; // Not a db table column//判斷注解類型if(anns[0] instanceof SQLInteger) {SQLInteger sInt = (SQLInteger) anns[0];//獲取字段對應列名稱,如果沒有就是使用字段名稱替代if(sInt.name().length() < 1)columnName = field.getName().toUpperCase();elsecolumnName = sInt.name();//構建語句columnDefs.add(columnName + " INT" +getConstraints(sInt.constraint()));}//判斷String類型if(anns[0] instanceof SQLString) {SQLString sString = (SQLString) anns[0];// Use field name if name not specified.if(sString.name().length() < 1)columnName = field.getName().toUpperCase();elsecolumnName = sString.name();columnDefs.add(columnName + " VARCHAR(" +sString.value() + ")" +getConstraints(sString.constraint()));}}//數據庫表構建語句StringBuilder createCommand = new StringBuilder("CREATE TABLE " + tableName + "(");for(String columnDef : columnDefs)createCommand.append("\n    " + columnDef + ",");// Remove trailing commaString tableCreate = createCommand.substring(0, createCommand.length() - 1) + ");";return tableCreate;}/*** 判斷該字段是否有其他約束* @param con* @return*/private static String getConstraints(Constraints con) {String constraints = "";if(!con.allowNull())constraints += " NOT NULL";if(con.primaryKey())constraints += " PRIMARY KEY";if(con.unique())constraints += " UNIQUE";return constraints;}public static void main(String[] args) throws Exception {String[] arg={"com.zejian.annotationdemo.Member"};for(String className : arg) {System.out.println("Table Creation SQL for " +className + " is :\n" + createTableSql(className));}}
}

推薦博客

  程序員寫代碼之外,如何再賺一份工資?

輸出結果:

Table Creation SQL for com.zejian.annotationdemo.Member is :
CREATE TABLE MEMBER(
ID VARCHAR(50) NOT NULL PRIMARY KEY,
NAME VARCHAR(30) NOT NULL,
AGE INT NOT NULL,
DESCRIPTION VARCHAR(150)
);

如果對反射比較熟悉的同學,上述代碼就相對簡單了,我們通過傳遞Member的全路徑后通過Class.forName()方法獲取到Member的class對象,然后利用Class對象中的方法獲取所有成員字段Field,最后利用field.getDeclaredAnnotations()遍歷每個Field上的注解再通過注解的類型判斷來構建建表的SQL語句。這便是利用注解結合反射來構建SQL語句的簡單的處理器模型。

?

轉載于:https://www.cnblogs.com/java-chen-hao/p/11024153.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/279364.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/279364.shtml
英文地址,請注明出處:http://en.pswp.cn/news/279364.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

打印墨水調鋼筆墨水_如何節省墨水并改善網站打印質量

打印墨水調鋼筆墨水Printing out web pages you want a hard copy of can be a little hit and miss. Unlike other documents, it is not easy to tell exactly how many pieces of paper will be needed, and whether or not there will be any awkward clipping. Add to thi…

highcharts 怎么去掉鼠標懸停效果_練瑜伽減肥沒效果什么原因?

沒有心的參與&#xff0c;瑜伽就不是瑜伽曾經有很多人問&#xff1a;自己想用瑜伽來減肥&#xff0c;但練習瑜伽這么久&#xff0c;為什么還是減不下來&#xff1f;一點效果都沒有。瑜伽是什么&#xff1f;瑜伽只是一種單純的運動嗎&#xff1f;只讓身體參與進去就可以了嗎&…

百度地圖1

百度地圖BMap的類 BMap的屬性是一些構造函數,主大類有&#xff1a;核心類、基礎類、控件類、覆蓋物類、右鍵菜單類、地圖類型類、地圖吐槽類、服務類、全局類 核心類Map Map&#xff1a;最主要的一個類&#xff0c;集成了其他模塊的方法&#xff0c;是一個集成了整個地圖功能的…

Java基礎學習總結(23)——GUI編程

2019獨角獸企業重金招聘Python工程師標準>>> 一、AWT介紹 所有的可以顯示出來的圖形元素都稱為Component&#xff0c;Component代表了所有的可見的圖形元素&#xff0c;Component里面有一種比較特殊的圖形元素叫Container&#xff0c;Container(容器)在圖形界面里面…

spring-使用配置文件完成JdbcTemplate操作數據庫

一、創建spring項目項目名稱&#xff1a;spring101302二、在項目上添加jar包1.在項目中創建lib目錄/lib2.在lib目錄下添加spring支持commons-logging.jarjunit-4.10.jarlog4j.jarmysql-connector-java-5.1.18-bin.jarspring-beans-3.2.0.RELEASE.jarspring-context-3.2.0.RELEA…

瓦片經緯度及行列號轉換_Slippy map tilenames(瓦片和經緯度換算)

Slippy map tilenames(瓦片和經緯度換算)This article describes the file naming conventions for theSlippy Map application.Tiles are 256 256 pixel PNG filesEachzoom level is a directory, each column is a subdirectory, andeach tile in that column is a fileFilen…

在Windows 7或Vista(或Windows 8.x,Sorta)上禁用Aero

The Windows Aero Glass interface for Windows 7 or Vista requires a decent video card, you won’t be able to use it on an old clunker computer. For those worried about performance, sometimes squeezing every last drop requires disabling Aero. Windows 7或Vist…

一個簡單的JDBC通用工具

支持多種數據庫&#xff0c;統一方式產生連接&#xff0c;最優化、最簡單方式釋放資源。歡迎拍磚&#xff01;import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.sql.*; import java.util.List; import java.util.Properties…

sfm點云代碼_VisualSFM使用方法與心得

關于VisualSfM的更多內容組合多個模型(What if VisualSFM produces multiple models?)&#xff1a;按照上述步驟進行稀疏重建后&#xff0c;理論上可以得到很好的模型。如果結果產生了多個模型&#xff0c;要想把多個模型合成成一個&#xff0c;點擊菜單中的“SfM->More Fu…

macos mojave_使Ubuntu看起來像macOS Mojave的黑暗模式

macos mojaveIf you’re a Linux user who likes the look of the dark mode coming in macOS Mojave, you’re in luck: there’s a GTK theme just for you. 如果您是Linux用戶&#xff0c;并且喜歡macOS Mojave中的黑暗模式外觀&#xff0c;那么您很幸運&#xff1a;這里有一…

html的列表標簽

列表一般應用在布局中的新聞標題列表和文章標題列表以及網頁菜單等。 例如這個就是一個列表&#xff1a; 列表標簽有幾種&#xff0c;分別是有序列表&#xff0c;無序列表&#xff0c;定義列表。 有序列表<!DOCTYPE html> <html lang"en"> <head>&…

撬鎖錘怎么用_安全錘是啥?消防蜀黍教你怎么選?如何快速破拆逃生?

逃生錘又叫安全錘&#xff0c;生活中很多地方都可以看到&#xff0c;公交車、地鐵窗邊都少不了它們的身影它的款式也是五花八門&#xff0c;那么問題來了當遇到突發狀況被困車內時&#xff0c;哪種破窗工具最有效&#xff1f;又該如何快速逃生自救&#xff1f;近日&#xff0c;…

WSUS技術概覽

WSUS新功能展示: 支持更多微軟產品更新-->Windows Office MS SQL Server Exchange ......基于產品及分類篩選下載更新的能力更多語言支持定位更新目標計算機或計算機組的能力-->分發前,測試更新; 保護運行特定應用程序的計算機; 靈活使用Deadline; ...... 見下…

Java基礎學習總結(16)——Java制作證書的工具keytool用法總結

2019獨角獸企業重金招聘Python工程師標準>>> 一、keytool的概念 keytool 是個密鑰和證書管理工具。它使用戶能夠管理自己的公鑰/私鑰對及相關證書&#xff0c;用于&#xff08;通過數字簽名&#xff09;自我認證&#xff08;用戶向別的用戶/服務認證自己&#xff09…

什么是文件擴展名?

A file extension, or filename extension, is a suffix at the end of a computer file. It comes after the period, and is usually two-four characters long. If you’ve ever opened a document or viewed a picture, you’ve probably noticed these letters at the end…

變量與常量

什么是變量/常量&#xff1f; 變量是計算機內存中的一塊區域&#xff0c;變量可以存儲規定范圍內的值&#xff0c;而且值可以改變。基于變量的數據類型&#xff0c;解釋器會分配指定內存&#xff0c;并決定什么數據可以被存儲在內存中。常量是一塊只讀的內存區域&#xff0c;常…

python藍牙編程_藍牙編程經典程序!

文檔從網絡中收集&#xff0c;已重新整理排版.word版本可編輯.歡迎下載支持.1word版本可編輯.歡迎下載支持.L2CAP socketsExample 4-4. l2cap-server.c#include #include #include #include #include int main(int argc, char **argv){struct sockaddr_l2 loc_addr { 0 }, rem…

[項目總結]在ios中使用soundtouch庫實現變聲

這篇文章是項目總結了。 做了段時間的項目&#xff0c;過程中也遇到了很多麻煩&#xff0c;但是好在終于都解決了&#xff0c;這里是這里是項目之后憑著記憶總結出來&#xff0c;大家有遇到同樣的問題&#xff0c;希望能參考了&#xff0c;但是我記憶可能不太好了&#xff0c;要…

Myeclipse優化配置

2019獨角獸企業重金招聘Python工程師標準>>> 作為企業級開發最流行的工具&#xff0c;用Myeclipse開發java web程序無疑是最合適的&#xff0c;java web前端采用jsp來顯示&#xff0c;myeclipse默認打開jsp的視圖有卡頓的現象&#xff0c;那么如何更改jsp默認的打開…

Java多線程之靜態代理

1 package org.study2.javabase.ThreadsDemo.staticproxy;2 3 /**4 * Date:2018-09-18 靜態代理 設計模式5 * 1、真實角色6 * 2、代理角色&#xff1a;持有真實角色的引用7 * 3、二者實現相同的接口8 * 舉例說明&#xff1a;Couple類和Company類都實現了Marry&#xff0c;…