我是一個從汽車行業轉行IT的項目經理,我是Edward,如想了解更多,請關注我的公衆號【轉行項目經理的逆襲之路】。學了兩天Spring框架,沒有感覺到任何便利,反而是一堆條條框框的註解把原本隨心所欲的程序邏輯變得不可控了,不測試一下你甚至不知道既定的操作到底完成了沒有。
1. 什麼是框架
框架在項目中的表現就是一系列的jar包,例如Thymeleaf就是一個框架。
每種框架都會解決某種特定的問題,可能是開發效率的問題,或運行效率的問題,或代碼管理維護的問題等等。
項目中使用框架就相當於得到了一個“毛坯房”,使用了框架之後,開發人員只需要關心後續的“裝修”即可。
絕大部分的框架都有特定的使用方式,在使用時,必須遵循框架的使用規則!
每個框架都可能是若干個開發人員甚至開發團隊多年的工作積累的作品,對於初學者來說,不要過於鑽牛角尖,嘗試理解框架的底層實現原理!
簡單的說:使用框架,可以讓編程變得更加簡單!在學習框架時,主要學習框架的正確使用方式!
2. 依賴關係
假設在項目中需要開發一個用戶註冊的功能!在項目中可能存在:
public class UserRegServlet {
private UserDao userDao = new UserDao();
public void doPost() {
userDao.reg(); // 調用userDao對象實現存儲用戶數據
}
}
public class UserDao {
public void reg() {
// 通過JDBC技術將用戶數據存儲到數據庫中
}
}
在以上代碼中,UserRegServlet
就是依賴於UserDao
的!
3. 耦合度
如果某個類過於依賴於另外一個類,通常稱之爲了“耦合度較高”,是不利於代碼的管理和維護的,簡單的說,如果UserRegServlet
依賴於UserDao
,在未來的某一天,UserDao
已經不能滿足項目的需求了(可能是因爲代碼有Bug,或者使用的技術比較落後等),如果需要把UserDao
替換掉,替換難度大就會影響項目的管理和維護,爲了解決這樣的問題採取的解決方案就稱之爲“解耦”,使得依賴關係不那麼明確,甚至就是不明確!
就以上UserRegServlet
依賴UserDao
的問題,如果要解耦,可以先創建一個接口:
public interface IUserDao {
void reg();
}
然後,使得UserDao
是實現了以上接口的:
public class UserDao implements IUserDao {
public void reg() {
// 具體的實現了reg()方法應該實現的功能
}
}
經過以上調整以後,如果在UserRegServlet
中需要使用到UserDao
,以前的代碼是這樣的:
private UserDao userDao = new UserDao();
現在就可以改爲:
private IUserDao userDao = new UserDao();
以上代碼就相當於:
private List<String> strings = new ArrayList<>();
改成這樣以後,在同一個項目中,無論多少個Servlet
組件需要使用到UserDao
,都可以使用以上“聲明爲接口,創建實現類的對象”的語法風格,如果以後UserDao
需要被替換掉,也只需要替換“賦值”的代碼,聲明部分是不需要替換的!例如需要把UserDao
替換爲UserMybatisDao
時,原來的代碼是:
private IUserDao userDao = new UserDao();
新的代碼就可以是:
public class UserMybatisDao implements IUserDao {
public void reg() {
// 使用更好的方式實現reg()應該實現的功能
}
}
在後續的使用中,就可以是:
private IUserDao userDao = new UserMybatisDao();
也就是說,在UserDao
換成了UserMybatisDao
時,在各個Servlet
中,都只需要調整等於號右側的內容,而不再需要修改等於號左側的部分!
當然,關於以上代碼的右側部分,還可以使用“工廠設計模式”作進一步的處理:
public class UserDaoFactory {
// 返回接口類型的對象
public static IUserDao newInstance() {
return new UserDao(); // 也可以返回UserMybatisDao的對象
}
}
當有了工廠後,此前的代碼就可以進一步調整爲:
private IUserDao userDao = UserDaoFactory.newInstance();
可以發現,以上代碼中不再出現任何一個實現類的名字了,無論是哪個Servlet
組件需要訪問數據庫,都聲明爲以上代碼即可,以後,如果實現類需要被替換,也只需要替換工廠方法的返回值即可!
在實際項目開發時,項目中的組件的依賴更加複雜,爲每個組件都創建對應的接口及工廠是非常麻煩的,而Spring框架就很好的解決了這個問題,可以簡單的將Spring理解爲一個“萬能工廠”,當使用了Spring框架後,就不必自行開發工廠了!
4. Spring框架簡介
Spring框架的主要作用:解決了創建對象和管理對象的問題。
5. 通過Spring創建對象
創建Maven Project,在創建過程中,勾選Create a simple project,Group Id填爲cn.tedu
,Artifact Id填爲spring01
,其它項保持默認即可。
使用Spring框架時,必須在項目的pom.xml中添加spring-context
的依賴:
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
首先,在項目中,創建cn.tedu.spring
包,並在這個包下創建BeanFactory
類:
package cn.tedu.spring;
public class BeanFactory {
}
當前,代碼放在哪個包中並不重要,應該養成習慣,每個類都應該放在某個包中,不要讓任何類不放在任何包中!
以上類的名稱也不重要,是自定義的!
如果希望由Spring來創建並管理某個類的對象,必須在以上類中添加方法,關於這個方法:
- 應該使用
public
權限; - 返回值類型就是需要Spring創建並管理的類的對象的類型;
- 方法名稱可以自定義;
- 參數列表暫時爲空;
- 在方法體中,自行編寫創建返回值對象的代碼。
假設需要Spring來創建Date
類型的對象,則在類中添加方法:
public Date aaa() {
// 規範,規則
}
Spring框架要求:創建對象的方法必須添加@Bean
註解,並且,這樣的方法必須在配置類中!任何一個類添加了@Configuration
註解都可以作爲配置類!
package cn.tedu.spring;
@Configuration
public class BeanFactory {
@Bean
public Date aaa() {
return new Date();
}
}
完成後,應該使用一個可以運行的類,或通過單元測試來檢驗“是否可以通過Spring容器獲取對象”。本次先創建一個可以運行的類:
package cn.tedu.spring;
public class SpringTests {
public static void main(String[] args) {
// 1. 加載配置類,得到Spring容器
AnnotationConfigApplicationContext ac
= new AnnotationConfigApplicationContext(BeanFactory.class);
// 2. 從Spring容器中獲取所需要的對象
Date date = (Date) ac.getBean("aaa"); // getBean()方法的參數就是創建對象的方法的名稱
// 3. 測試獲取到的對象
System.out.println(date);
// 4. 關閉
ac.close();
}
}
6. 關於@Bean註解
當方法的聲明之前添加了@Bean
註解,就表示這個方法是需要由Spring框架所調用,並且,由Spring框架管理該方法返回的對象的!默認情況下,該方法的名稱就是後續獲取對象時,調用getBean()
方法的參數!
由於添加了@Bean
註解的方法是被Spring框架調用的,不需要自行編寫代碼來調用這個方法,所以,Spring的建議是“使用合理的屬性名稱作爲方法名,並不需要使用動詞或動詞爲前綴的方法名”,簡單的說,如果方法是爲了獲取Date
類型的對象的,該方法的名稱應該是date
,而不是getDate()
,則後續調用getBean()
時,參數就是date
這個名稱!
當然,如果不遵循Spring的建議,還可以在@Bean
註解中配置註解參數來指定Bean的名稱,例如:
@Bean("date")
public Date getDate() {
return new Date();
}
則後續就根據註解參數來獲取對象:
Date date = (Date) ac.getBean("date");
其關係如下圖:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-98tspAXI-1590056665138)(01.png)]
其實,在開發項目時,真的不必關心這些問題,也就是說,例如是一個獲取Date
對象的方法,其名稱到底是date
還是getDate
都是正確的!畢竟這個方法最終就是由Spring框架來調用,開發人員不會自行調用該方法!
7. Spring管理對象的作用域
由Spring管理的對象,默認情況下,是單例的!所以,其作用域就非常久!
在Spring管理對象的情況下,討論對象的作用域,其本質就是討論其是否單例!
在創建對象的方法之前,添加@Scope
註解,並配置註解參數爲prototype
,就可以使得該對象不是單例的:
@Scope("prototype")
@Bean
public User user() {
return new User();
}
由Spring管理的對象,如果是單例模式的,默認情況下,是餓漢式的!在創建對象的方法之前,添加@Lazy
註解,就可以調整爲懶漢式的:
@Bean
@Lazy
public User user() {
return new User();
}
一般,在開發項目時,極少調整對象的作用域!
8. 當天小結:
- Spring的主要作用:創建對象,管理對象;
- 如果某個方法是用於給Spring框架創建對象的,這個方法就必須添加
@Bean
註解; - 所有添加了
@Bean
註解的方法,其所在的類應該添加@Configuration
註解,凡添加了@Configuration
註解的類稱之爲配置類; - 默認情況下,由Spring管理的對象是單例的,使用
@Scope
註解可以將Spring管理的對象調整爲“非單例”的; - 默認情況下,由Spring管理的單例的對象是是“餓漢式”的,使用
@Lazy
可以將它們改爲“懶漢式”的。
附1:設計模式之單例模式
單例模式的特點:在同一時期,某個類的對象一定最多隻有1個!也許會嘗試多次的獲取對象,但是,獲取到的一定是同一個對象!
假設項目中有King
類:
public class King {
}
很顯然,目前它並不是單例的,因爲,可以:
King k1 = new King();
King k2 = new King();
King k3 = new King();
以上代碼就創建了3個King
類型的對象!如果要實現單例,首先,就必須限制構造方法的訪問,例如:
public class King {
private King() {
}
}
每個類中都可以有若干個構造方法,如果某個類沒有顯式的聲明任何構造方法,編譯器就會自動添加1個公有的、無參數的構造方法!如果類中已經聲明任何構造方法,則編譯器不會自動添加構造方法!
由於將構造方法聲明爲私有的,則原有的King k1 = new King();
這類代碼就不能用於創建對象了!
限制構造方法的訪問,其目的是“不允許隨意創建對象”,並不是“不允許創建對象”,在King
類的內部,還是可以創建對象的,可以添加方法,返回內部創建的對象:
public class King {
private King king = new King();
private King() {
}
public King getInstance() {
return king;
}
}
所以,當需要King
類型的對象時,可以通過getInstance()
方法來獲取!
但是,以上代碼是不可行的!因爲,如果要調用getInstance()
方法,必須先獲取King
的對象,而獲取King
對象的唯一方式就是調用getInstance()
方法!爲了解決這個問題,必須在getInstance()
方法的聲明之前添加static
修飾符,最終,就可以通過類名.方法名()
的語法格式來調用方法了!同時,由於“被static
修飾的成員,不可以訪問其它未被static
修飾的成員”,所以,全局屬性king
也必須被static
修飾:
public class King {
private static King king = new King();
private King() {
}
public static King getInstance() {
return king;
}
}
至此,基本的單例模式的代碼就設計完成了!
以上代碼是“餓漢式”的單例模式,另外,還有“懶漢式”的單例模式!
基本的懶漢式單例模式的代碼是:
public class King {
private static King king = null;
private King() {
}
public static King getInstance() {
if (king == null) {
king = new King();
}
return king;
}
}
注意:以上代碼是多線程不安全的!
在開發領域中,只要數據的產生、變化不是開發人員預期的,就稱之爲“不安全”,也就是“數據安全問題”。
爲了保障線程安全,應該爲以上創建對象的代碼片斷“加鎖”,例如:
public class King {
private static King king = null;
private King() {
}
public static King getInstance() {
synchronized ("hello") {
if (king == null) {
king = new King();
}
}
return king;
}
}
當然,無論是哪個線程在什麼時候執行以上代碼,都必須先“鎖住”代碼片斷後才能開始執行,是沒有必要的,“鎖”的性能消耗是浪費的,所以,可以進一步調整爲:
public class King {
private static King king = null;
private King() {
}
public static King getInstance() {
if (king == null) { // 判斷有沒有必要鎖定接下來的代碼
synchronized ("java") {
if (king == null) { // 判斷有沒有必要創建對象
king = new King();
}
}
}
return king;
}
}
至此,懶漢式的單例模式就完成了!
9. 由Spring管理的對象的生命週期
如果需要管理Bean的生命週期,可以在對應的類中自定義生命週期的初始化方法和銷燬方法,關於這2個方法的聲明:
- 應該使用
public
權限; - 使用
void
表示返回值類型; - 方法名稱可以自定義;
- 參數列表爲空。
例如:
package cn.tedu.spring;
public class User {
public User() {
System.out.println("User.User()");
}
public void init() {
System.out.println("User.init()");
}
public void destroy() {
System.out.println("User.destroy()");
}
}
在配置Spring管理對象的@Bean
註解中,配置註解參數,以指定以上2個方法分別是初始化方法和銷燬方法:
package cn.tedu.spring;
@Configuration
public class BeanFactory {
@Bean(initMethod = "init", destroyMethod = "destroy")
public User user() {
return new User();
}
}
最終,可以看到:
- 初始化方法會在構造方法之後執行,且只執行1次;
- 銷燬方法會在Spring容器被銷燬之前執行,且只執行1次。
10. 使用組件掃描使得Spring管理類的對象
首先,自定義某個類(類名、包名均沒有要求),在類的聲明之前添加@ComponentScan
註解,該註解用於配置組件掃描,註解的參數是String
類型的,表示“被掃描的根包”:
package cn.tedu.spring;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan("cn.tedu.spring")
public class SpringConfig {
}
在組件掃描的包下創建類,該類的聲明之前需要添加@Component
註解,以表示這個類是一個“組件類”,後續,當Spring掃描時,會自動創建所有組件類的對象:
package cn.tedu.spring;
import org.springframework.stereotype.Component;
@Component
public class User {
}
當完成以後配置後,後續,程序執行時,只要加載了SpringConfig
類,由於類之前配置了組件掃描,Spring框架就會掃描對應的包下所有的類,並逐一檢查是否爲“組件類”,如果是,則創建對象,如果不是,則不創建!
使用@ComponentScan
時,配置的是需要掃描的“根包”,假設需要掃描的是cn.tedu.spring
,在配置時,配置爲cn.tedu
甚至配置爲cn
都是可用的,但是,強烈不推薦使用過於簡單的設置,避免出現掃描範圍過多而導致的浪費資源!
另外,在@ComponentScan
註解的源代碼中:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
}
可以看出,配置的值可以是String[]
,也就是可以指定多個包名。
在使用這種做法時,必須保證被Spring管理的對象所歸屬的類存在無參數構造方法!
在使用這種做法時,Spring創建對象後,默認會使用以下原則作爲Bean的名稱:
- 如果類名的第1個字母是大寫的,第2個字母是小寫的(不關心其它字母的大小寫),則會把類名的第1個字母改爲小寫,其它不變,作爲Bean的名稱,例如類名是
User
時,Bean的名稱就是user
,類名是UserDao
時,Bean的名稱就是userDao
; - 如果不滿足以上條件,則類名就是Bean的名稱。
如果希望使用自定義的名稱作爲Bean的名稱,可以在@Component
註解中配置參數,例如:
package cn.tedu.spring;
import org.springframework.stereotype.Component;
@Component("uuu")
public class User {
}
則後續調用getBean()
方法時,就必須使用"uuu"
作爲參數來獲取對象!
在Spring框架的作用範圍內,除了@Component
以外,另外還有3個註解,可以起到完全等效的效果:
@Controller
:通常添加在控制器類的聲明之前;@Service
:通常添加在業務類的聲明之前;@Repository
:通常添加在**持久層的類(負責數據的持久化管理)**的聲明之前。
也就是說,這4種註解作用、用法完全相同,只是語義不同。
目前,已經介紹了2種使得Spring框架管理類的對象的做法:
- 自定義方法返回某個對象,並在方法的聲明之前添加
@Bean
註解; - 將類放在組件掃描的包或其子孫包中,並在類的聲明之前添加
@Component
/@Controller
/@Service
/@Repository
註解。
以上的第1種做法是萬能的,適用於任何條件,但是,在設計代碼時相對麻煩,管理起來相對不便利;而第2種做法就更加簡單、直觀,卻只適用於自定義的類。
所以,只要是自行編寫的類,都應該採取第2種做法,如果需要Spring管理其它類(JDK中的,或某框架中的)的對象,只能使用第1種做法!
11. 使用Spring讀取.properties文件
假設在項目的src/main/resources下存在jdbc.properties文件,其內容是:
url=jdbc:mysql://localhost:3306/db_name
driver=com.mysql.jdbc.Driver
然後,在項目中,自定義某個類,在這個類中,聲明對應數量的屬性,這些屬性的值將會是以上配置信息的值!
public class JdbcProperties {
private String url;
private String driver;
// 生成以上2個屬性的Getters & Setters
}
當需要讀取以上jdbc.properties配置文件時,需要在以上類的聲明之前添加@PropertySource
註解,並配置需要讀取的文件的位置:
// 以下註解的參數是配置文件的名稱
@PropertySource("jdbc.properties")
public class JdbcProperties {
private String url;
private String driver;
// 生成以上2個屬性的Getters & Setters
}
接下來,就可以把讀取到的值賦值給類中的2個屬性,可以通過@Value
註解來實現:
// 以下註解的參數是配置文件的名稱
@PropertySource("jdbc.properties")
public class JdbcProperties {
@Value("${url}") // 在註解參數的大括號的值,是jdbc.properties配置中等於號左側的名稱
private String url;
@Value("${driver}")
private String driver;
// 生成以上2個屬性的Getters & Setters
}
最後,整個的讀取過程是由Spring框架來完成的,所以,以上JdbcProperties
類還應該被Spring框架所管理,可以採取組件掃描的做法,則創建SpringConfig
類,用於指定組件掃描的包:
// 以下註解參數配置的就是組件掃描的包,同時,請保證JdbcProperties類是在這個包或其子孫包中的
@ComponentScan("cn.tedu.spring")
public class SpringConfig {
}
然後,在JdbcProperties
類的聲明之前,補充添加@Component
註解,使得Spring框架掃描到這個類時,能明確的知道“這個類是組件類”,從而創建該類的對象:
@Component
// 以下註解的參數是配置文件的名稱
@PropertySource("jdbc.properties")
public class JdbcProperties {
@Value("${url}") // 在註解參數的大括號的值,是jdbc.properties配置中等於號左側的名稱
private String url;
@Value("${driver}")
private String driver;
// 生成以上2個屬性的Getters & Setters
}
全部完成後,可以自定義某個類,用於測試運行:
package cn.tedu.spring;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class SpringTests {
public static void main(String[] args) {
// 1. 加載配置類,得到Spring容器
AnnotationConfigApplicationContext ac
= new AnnotationConfigApplicationContext(SpringConfig.class);
// 2. 從Spring容器中獲取對象
JdbcProperties jdbcProperties
= (JdbcProperties) ac.getBean("jdbcProperties");
// 3. 測試
System.out.println(jdbcProperties.getUrl());
System.out.println(jdbcProperties.getDriver());
// 4. 關閉
ac.close();
}
}
注意:在類似於jdbc.properties這樣的配置文件中,如果某個屬性的名稱是username
,且最終項目是在Windows操作系統的平臺上運行時,讀取到的值將是“當前登錄Windows系統的系統用戶名稱”,而不是jdbc.properties文件中配置的屬性值!所以,一般推薦在編寫jdbc.properties這類配置文件時,各屬性之前最好都添加一些特有的前綴,使得屬性名一定不與某些關鍵名稱發生衝突,例如:
project.jdbc.url=jdbc:mysql://localhost:3399/db_name
project.jdbc.driver=com.mysql.jdbc.Driver
project.jdbc.username=root
project.jdbc.password=1234
並且,在使用@Value
註解時,也配置爲以上各個等於號左側的完整名稱:
@Component
@PropertySource("jdbc.properties")
public class JdbcProperties {
@Value("${project.jdbc.url}")
private String url;
@Value("${project.jdbc.driver}")
private String driver;
@Value("${project.jdbc.username}")
private String username;
@Value("${project.jdbc.password}")
private String password;
// Getters & Setters
}
最後,使用Spring框架時,如果屬性的值是由Spring框架進行賦值的,Spring框架會自動的處理數據類型的轉換,所以,在聲明屬性時,聲明爲所期望的類型即可,例如,在配置文件中存在:
project.jdbc.initialSize=5
project.jdbc.maxTotal=20
這2個屬性分別表示“初始化連接數”和“最大連接數”,應該是數值類型的,在類中聲明屬性時,就可以使用int
或Integer
類型:
@Value("${project.jdbc.initialSize}")
private int initialSize;
@Value("${project.jdbc.maxTotal}")
private int maxTotal;
當然,必須保證類型的轉換是可以成功的,例如數字5
既可以轉換爲String
,又可以是int
或Integer
,所以,聲明以上initialSize
時,這幾個數據類型都是可用的,根據使用需求進行選取即可!
另外,還有另一種做法讀取**.properties**類型的文件,就是使用@Autowired
註解爲Environment
類型的屬性自動賦值:
@Component
@PropertySource("jdbc.properties")
public class JdbcProperties {
@Autowired
private Environment environment;
public Environment getEnvironment() {
return environment;
}
public void setEnvironment(Environment environment) {
this.environment = environment;
}
}
最終,測試運行:
package cn.tedu.spring;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class SpringTests {
public static void main(String[] args) {
// 1. 加載配置類,得到Spring容器
AnnotationConfigApplicationContext ac
= new AnnotationConfigApplicationContext(SpringConfig.class);
// 2. 從Spring容器中獲取對象
JdbcProperties jdbcProperties
= (JdbcProperties) ac.getBean("jdbcProperties");
// 3. 測試
System.out.println(jdbcProperties.getEnvironment().getProperty("project.jdbc.url"));
System.out.println(jdbcProperties.getEnvironment().getProperty("project.jdbc.driver"));
System.out.println(jdbcProperties.getEnvironment().getProperty("project.jdbc.username"));
System.out.println(jdbcProperties.getEnvironment().getProperty("project.jdbc.password"));
System.out.println(jdbcProperties.getEnvironment().getProperty("project.jdbc.initialSize"));
System.out.println(jdbcProperties.getEnvironment().getProperty("project.jdbc.maxTotal"));
// 4. 關閉
ac.close();
}
}
可以看到,使用這種做法時,Spring框架會把讀取到的所有配置信息都封裝到了Environment
類型的對象中,當需要獲取某個配置值時,調用Environment
對象的getProperty()
方法再獲取,同時,getProperty()
方法返回的是String
類型的數據,如果希望的數據類型不是String
,則需要開發人員自行轉換類型!
一般,還是推薦使用@Value
註解逐一讀取各配置值,使用起來更加靈活一些!
抽象類與接口的區別
1. 共同點
都可以包含抽象方法;
2. 區別
- 抽象類是一種“類”,是使用
class
作爲關鍵字來聲明的;而接口是另一種數據,是使用interface
作爲關鍵字來聲明的; - 抽象類中可以有各種權限不同、修飾符不同的屬性,也可以包含普通方法、抽象方法,或者完全沒有普通方法,或者完全沒有抽象方法;而接口中的所有成員都是
public
的,所有屬性都是static
、final
的,在JDK 1.8之前,所有的方法都是抽象的; - 普通的類與抽象類的關係是“繼承”的關係,當普通的類繼承了抽象類後,就有義務重寫抽象類中的抽象方法,在Java語句中,類之間的繼承是1對1的關係;普通的類與接口的關係是”實現“的關係,當普通的類實現了接口後,也有義務重寫接口中的所有抽象方法,類與接口的實現關係是1對多的,即1個類可以同時實現若干個接口;接口與接口之間也可以存在繼承關係,且是1對多的關係,即某1個接口可以同時繼承若干個接口;
3. 使用心得 / 裝
類,是描述”類別“的;接口,是描述形爲模式、行爲特徵、規範、標準的!
類與類之間是is a
的關係;類與接口之間是has a
的關係。
public class Person { public String name; }
public class Student extends Person {}
public class Teacher extends Person {}
public class Animal { }
public class Cat extends Animal {}
public interface 學習 { void 學習(某參數); }
public interface 授課 {}
public interface 駕駛 { void 駕駛(某參數); }
public class Person implements 學習, 授課, 駕駛 {}
Person 張三 = new Person();
Person 李四 = new Person();