荐:Java常见设计模式
設計模式的原則:
1、開閉原則 對擴展開放,對修改關閉。?
2、依賴倒置原則 通過抽象使各個類或者模塊不相互影響,實現松耦合。?
3、單一職責原則 一個類、接口、方法只做一件事。?
4、接口隔離原則 盡量保證接口的純潔性,客戶端不應該依賴不需要的接口。?
5、迪米特法則 又叫最少知道原則,一個類對其所依賴的類知道得越少越好。
6、里氏替換原則 子類可以擴展父類的功能但不能改變父類原有的功能。?
7、合成復用原則 盡量使用對象組合、聚合,而不使用繼承關系達到代碼復用的目的。
?
代碼地址: https://github.com/ZhangYDevelop/java-design-pattern.git
一、工廠模式
package?com.zy.java.design.patterns.factory;/***?@AUTHOR?zhangy*?2020-10-11??20:22*?工廠模式(simple?factory?pattern)?,列子:Spring?中的BeanFactory*/ public?class?FactoryPattern?{/***?生產汽車的工廠*/private?static??class?CarFactory?{public??static?Car?getCarInstance(String?type)?{if?(ICar.BEN_CHI.equals(type))?{return??new?BenChiCar().createCar();}if?(ICar.DA_ZHONG.equals(type))?{return??new?DaZhongCar().createCar();}return??null;}}/***?制造商*/private?static?class?BenChiCar?implements?ICar?{public?Car?createCar()?{System.out.println("生產奔馳車。。。");return?new?Car();}}/***?制造商*/private?static??class?DaZhongCar?implements?ICar?{public?Car?createCar()?{System.out.println("生產大眾車...");return?new?Car();}}/***?對生產車輛抽象*/private?interface?ICar?{String?BEN_CHI?=?"BEN_CHI";String?DA_ZHONG?=?"DA_ZHONG";Car?createCar();}/***?物質抽象*/private??static??class?Car?{private?String?whell;private?String?engine;}public?static?void?main(String[]?args)?{CarFactory.getCarInstance(ICar.DA_ZHONG);} }?
二、單列模式(提供一個全局訪問點)
public?class?Singleton?{//?volatile?修飾,線程可見private?volatile?static?Singleton??singleton=?null;//?私有化構造方案private?Singleton()?{if?(null?!=?singleton)?{throw?new?RuntimeException("單列不允許多實例"); //????????????//防止以下流氓方法 //????????????Class<?>?clazz?=?Singleton.class; //????????????try?{ //????????????????//?通過反射獲取構造方法 //????????????????Constructor?construct?=??clazz.getDeclaredConstructor(null); //????????????????//?強制訪問 //????????????????construct.setAccessible(true); //????????????????//?實例化對象 //????????????????Object?o1?=?construct.newInstance(); //????????????????Object?o2?=?construct.newInstance(); //????????????}?catch?(Exception?e)?{ //????????????????e.printStackTrace(); //????????????}}}/***?雙重檢查*?@return*/public?static?final?Singleton??getInstance()?{if?(null?==?singleton)?{synchronized?(Singleton.class)?{if?(null?==?singleton)?{singleton?=?new?Singleton();}}}return?singleton;}/***?通過內部類獲取*?@return*/public?static?final?Singleton??getInstanceExt()?{//在返回結果以前,一定會先加載內部類return?SingleHolder.singleton;}/***?防止序列化破快單列*?@return*/private?Object?readResolve(){return?singleton;}private?static?class?SingleHolder?{private?static?final?Singleton?singleton?=?new?Singleton();} }?
三、原型模式(Prototype Pattern)
?
原型實例指定創建對象的種類,并且通過拷貝這些原型創建新的對象。
應用場景:
1、類初始化消耗資源較多。
2、new 產生的一個對象需要非常繁瑣的過程(數據準備、訪問權限等)
3、構造函數比較復雜。
4、循環體中生產大量對象時。
在Spring 中,原型模式應用得非常廣泛。例如scope=“prototype”,在我們經常用
的JSON.parseObject()也是一種原型模式
/***?原型模式(Prototype?Pattern),相當于拷貝*/ public?class?PrototypePattern?{public?interface?ProtoType?{ProtoType?clone();ProtoType?deepClone();}public?static?class?PrototypeA?implements?ProtoType?{int?age;String?name;public?int?getAge()?{return?age;}public?void?setAge(int?age)?{this.age?=?age;}public?String?getName()?{return?name;}public?void?setName(String?name)?{this.name?=?name;}//?淺克隆public?ProtoType?clone()?{PrototypeA?prototypeA?=?new?PrototypeA();prototypeA.setAge(this.age);prototypeA.setName(this.name);return?prototypeA;}//?深克隆public?ProtoType?deepClone()?{try{ByteArrayOutputStream?bos?=?new?ByteArrayOutputStream();ObjectOutputStream?oos?=?new?ObjectOutputStream(bos);oos.writeObject(this);ByteArrayInputStream?bis?=?new?ByteArrayInputStream(bos.toByteArray());ObjectInputStream?ois?=?new?ObjectInputStream(bis);ProtoType?copy?=?(ProtoType)ois.readObject();return?copy;}catch?(Exception?e){e.printStackTrace();return?null;}}}public?static?void?main(String[]?args)?{PrototypeA?prototypeA?=?new?PrototypeA();prototypeA.setName("zhangsan");prototypeA.setAge(12);ProtoType?protoType?=?prototypeA.clone();} }?
四、代理模式(Proxy Pattern)
該模式為其他對象提供一種代理, 以控制對這個對象的訪問。??
?Jdk動態代理
import?sun.misc.ProxyGenerator;import?java.io.*; import?java.lang.reflect.InvocationHandler; import?java.lang.reflect.Method; import?java.lang.reflect.Proxy; /***?Jdk?動態代理*/ public?class?JdkProxy??implements?InvocationHandler?{/***?被代理類必須實現接口*/Object?target;public??Object??getInstance(Object?object)?{this.target?=?object;Class<?>?clazz?=?target.getClass();return?Proxy.newProxyInstance(clazz.getClassLoader(),?clazz.getInterfaces(),?this);}@Overridepublic?Object?invoke(Object?proxy,?Method?method,?Object[]?args)?throws?Throwable?{before();Object?retObj?=?method.invoke(target,?args);after();return?retObj;}/***?可以模擬spring的事務開啟*/private?void?after()?{System.out.println("開啟事務。。。");}/***?可以模擬spring的事務提交*/private?void?before()?{System.out.println("提交事務。。。。");}public?interface?CrudHandler{Object?addObject(Object?o);void?delete(String?id);}public?static?class?CrudHandlerImpl?implements?CrudHandler?{@Overridepublic?Object?addObject(Object?o)?{System.out.println("向數據庫添加一條數據");return?null;}@Overridepublic?void?delete(String?id)?{System.out.println("刪除數據庫一條數據,?id="?+?id?);}}public?static?void?main(String[]?args)?{CrudHandler?crudHandler?=?(CrudHandler)new?JdkProxy().getInstance(new?CrudHandlerImpl());crudHandler.delete("dfdfdfdfdf");byte[]?bytes?=???ProxyGenerator.generateProxyClass("$Proxy0",?new?Class[]{CrudHandler.class});try?{FileOutputStream?fos?=?new?FileOutputStream("C://zhangyu//$Proxy0.class");fos.write(bytes);fos.close();}?catch?(Exception?e)?{e.printStackTrace();}} }????我們讀取代理類$Proxy0并寫到磁盤,通多 jad 命令來反編譯文件(jad? $Proxy0.class? proxy0.jad),就可以很清楚的看到jdk動態代理的原理,也很容易理解為什么jdk代理為什么只能代理接口。
//?Decompiled?by?Jad?v1.5.8g.?Copyright?2001?Pavel?Kouznetsov. //?Jad?home?page:?http://www.kpdus.com/jad.html //?Decompiler?options:?packimports(3)?import?java.lang.reflect.*;public?final?class?$Proxy0?extends?Proxyimplements?JdkProxy.CrudHandler {public?$Proxy0(InvocationHandler?invocationhandler){super(invocationhandler);}###########沈略部分代碼########public?final?Object?addObject(Object?obj){try{return?(Object)super.h.invoke(this,?m4,?new?Object[]?{obj});}catch(Error?_ex)?{?}catch(Throwable?throwable){throw?new?UndeclaredThrowableException(throwable);}}public?final?void?delete(String?s){try{super.h.invoke(this,?m3,?new?Object[]?{s});return;}catch(Error?_ex)?{?}catch(Throwable?throwable){throw?new?UndeclaredThrowableException(throwable);}}###########沈略部分代碼######## }?
?Cglib代理,它是通過動態繼承目標對象 實現的動態代理
import?net.sf.cglib.proxy.Enhancer; import?net.sf.cglib.proxy.MethodInterceptor; import?net.sf.cglib.proxy.MethodProxy;import?java.lang.reflect.Method;/***?Cglib?動態代理*/ public?class?CglibProxy??implements?MethodInterceptor{public?Object?getCglibProxyInstance(Class<?>?clazz)?{Enhancer?enhancer?=?new?Enhancer();//要把哪個設置為即將生成的新類父類enhancer.setSuperclass(clazz);enhancer.setCallback(this);return?enhancer.create();}@Overridepublic?Object?intercept(Object?o,?Method?method,?Object[]?objects,?MethodProxy?methodProxy)?throws?Throwable?{before();Object?retObj?=?methodProxy.invokeSuper(o,objects);after();return?retObj;}/***?可以模擬spring的事務開啟*/private?void?after()?{System.out.println("開啟事務。。。");}/***?可以模擬spring的事務提交*/private?void?before()?{System.out.println("調校事務。。。。");}public?static?void?main(String[]?args)?{Hanlder?handler?=??(Hanlder)new?CglibProxy().getCglibProxyInstance(Hanlder.class);handler.delete("dfdfdfd");}}public?class??Hanlder?{public?void?delete(String?id)?{System.out.println("刪除數據庫一條數據,?id="?+?id?);}}?
CGLib和JDK動態代理對比
????1、JDK動態代理是實現了被代理對象的接口,CGLib是繼承了被代理對象。
????2、JDK和CGLib都是在運行期生成字節碼,JDK是直接寫Class 字節碼,CGLib使用ASM框架寫Class字節碼,Cglib代理實現? 更復雜,生成代理類比JDK效率低。
????3、JDK調用代理方法,是通過反射機制調用,CGLib是通過 FastClass機制直接調用方法,CGLib執行效率更高。
????4、CGLib無法代理final修飾的方法,也無法代理內部類。
?
Spring 中的代理選擇原則?
1、當 Bean 有實現接口時,Spring 就會用 JDK 的動態代理?
2、當 Bean 沒有實現接口時,Spring 選擇 CGLib。
?
五、委派模式(Delegate Pattern)
該模式不屬于GOF23 種設計模式中。委派模式(Delegate Pattern)的基本作用就是負責任務的調用和分配任務,跟代理模式很像,可以看做是一種特殊情況下的靜態代理的全權代理,但是代理模式注重過程,而委派模式注重結果。在springMVC中DispatcherServlet就是很好例子
import?java.util.HashMap; import?java.util.Map;/***?委派模式*?簡單列子:老板叫領導干活*/ public?class?DelegatePattern?{/***?員工接口*/public?interface?Employee?{void???doSomething(String?commond);}/***?打印員*/public?static?class?EmployeeA?implements?Employee{@Overridepublic?void?doSomething(String?commond)?{System.out.println("打印員接到通知:"?+?commond);}}/***?前臺*/public?static??class?EmployeeB?implements?Employee{@Overridepublic?void?doSomething(String?commond)?{System.out.println("前臺接到命令:"?+?commond);}}/***?領導,不干活,指揮別人干*/public?static?class?Leader?implements?Employee?{private?Map<String,?Employee>?employees??=?new?HashMap<>();public?Leader()?{employees.put("前臺",?new?EmployeeB());employees.put("打印員",?new?EmployeeA());}@Overridepublic?void?doSomething(String?commond)?{employees.get(commond).doSomething(commond);}}/***?老板下達命令*/public?static?class?Boss?{public?void?commond(String?commond)?{new?Leader().doSomething(commond);}}public?static?void?main(String[]?args)?{//客戶請求(Boss)、委派者(Leader)、被被委派者(Target)//委派者要持有被委派者的引用//代理模式注重的是過程,?委派模式注重的是結果//策略模式注重是可擴展(外部擴展),委派模式注重內部的靈活和復用//委派的核心:就是分發、調度、派遣//委派模式:就是靜態代理和策略模式一種特殊的組合new?Boss().commond("前臺");} }六、策略模式(Strategy Pattern)
import?java.sql.ParameterMetaData; import?java.util.HashMap; import?java.util.Map;/***?策略模式*?簡單列子:支付方式的選擇*/ public?class?StrategyPattern?{/***?支付抽象*/public?interface?Payment?{void?pay(int?mount);}/***?阿里支付*/public?static?class?Alipay?implements?Payment?{@Overridepublic?void?pay(int?mount)?{System.out.println("阿里支付,支付金額:"?+?mount);}}/***?微信支付*/public?static?class?Wechatpay?implements?Payment?{@Overridepublic?void?pay(int?mount)?{System.out.println("微信支付,支付金額:"?+?mount);}}public?static?class?PaymentFactory?{private?static?Map<String,?Payment>??payMentMap??=?new?HashMap<>();private??static??Payment?defaultPayMent?=?new?Alipay();public?PaymentFactory()?{payMentMap.put(PayType.ALI_PAY,?new?Alipay());payMentMap.put(PayType.WECHAT_PAY,?new?Wechatpay());}public?static?Payment?getPayMent(String?type)?{Payment?payment?=?payMentMap.get(type);return??payment?==?null???defaultPayMent?:?payment;}}public?interface?PayType?{public?static?final?String?ALI_PAY?=?"ALI_PAY";public?static?final?String?WECHAT_PAY?=?"WECHAT_PAY";}public?static?void?main(String[]?args)?{Payment?payment?=??PaymentFactory.getPayMent(PayType.WECHAT_PAY);payment.pay(234);} }?
七、模版方法模式
package?com.zy.java.design.patterns.template;/***?@AUTHOR?zhangy*?2020-10-11??14:22*?模版方法模式(Template?Method?Pattern)是指定義一個算法的骨?架,并允許子類為一*?個或者多個步驟提供實現。*/ public?abstract?class?TemplateMethodPattern?{/***?創建訂單*?@param?info*/protected?final?void??createOrder(Object?info)?{//?創建訂單saveOrderInfo(info);//?支付pay(info);//?產生物流信息saveWuliuInfo(info);//?發送短信給用戶sendMessage(info);//?.....}private?final??void?sendMessage(Object?info)?{System.out.println("發送短信");}protected?final?void?saveWuliuInfo(Object?info)?{System.out.println("產生物流信息");}/***?交給不同的子類實現?可能有不同的支付方式:支付寶?微信?。。。*?@param?info*/protected?abstract?void?pay(Object?info);protected?final?void?saveOrderInfo(Object?info)?{System.out.println("產生一條訂單:insert?into?order_info?....");} }?模板模式的優缺點?
優點:?
1、利用模板方法將相同處理邏輯的代碼放到抽象父類中,可以提高代碼的復用性。?
2、將不同的代碼不同的子類中,通過對子類的擴展增加新的行為,提高代碼的擴展性。
3、把不變的行為寫在父類上,去除子類的重復代碼,提供了一個很好的代碼復用平臺, 符合開閉原則。?
缺點:?
1、類數目的增加,每一個抽象類都需要一個子類來實現,這樣導致類的個數增加。?
2、類數量的增加,間接地增加了系統實現的復雜度。?
3、繼承關系自身缺點,如果父類添加新的抽象方法,所有子類都要改一遍。 模板方法模式比較簡單,相信小伙伴們肯定能學會,也肯定能理解好!只要勤加練習, 多結合業務場景思考問題,就能夠把模板方法模式運用好。
?
八 適配器模式(adapter pattern)
?一個類的接口轉換成客戶期望的另一個接口,使原本的接口不兼容的類可以一起工作,屬于結構型設計模式
/***?適配器模式(adapter?pattern),不改變原有規則的前提下適應新的規則需求*/ public?class?AdaptePattern?{public?static??class?LoginService?{public?Object?login(String?userName,?String?password)?{System.out.println("賬號密碼登錄");return??new?Object();}}/***?登錄業務適配的抽象*/public?interface?LoginAdapter?{Object?login(LoginAdapter?adapter);boolean?support(Object?adapter);}public?static??class?LoginForQQ?implements?LoginAdapter?{public?Object?login(LoginAdapter?adapter)?{System.out.println("qq?登錄");return?new?Object();}public?boolean?support(Object?adapter)?{return?adapter?instanceof?LoginForQQ;}}public?static?class?LoginForWechat?implements?LoginAdapter?{public?Object?login(LoginAdapter?adapter)?{System.out.println("wechat?登錄");return?new?Object();}public?boolean?support(Object?adapter)?{return?adapter?instanceof?LoginForWechat;}}/***?第三方登錄*/public?interface?LoginForthirdPart?{Object?loginForQQ();Object?loginForWechat();}/***擴展原有的業務邏輯,不改變原有代碼,適應新的需求*/public?static?class?LoginForthirdPartService??extends?LoginService?implements?LoginForthirdPart?{public?Object?loginForQQ()?{return?doLogin(LoginForQQ.class);}public?Object?loginForWechat()?{return?doLogin(LoginForWechat.class);}private?Object??doLogin?(Class<??extends??LoginAdapter>?clazz)?{try?{LoginAdapter?adapter?=?clazz.newInstance();if?(adapter.support(adapter)?)?{return??adapter.login(adapter);}?else?{return??null;}}?catch?(InstantiationException?e)?{e.printStackTrace();}?catch?(IllegalAccessException?e)?{e.printStackTrace();}return??null;}}public?static?void?main(String[]?args)?{new?LoginForthirdPartService().loginForWechat();}}?
?適配器模式的優缺點?
????優點:?
????1、能提高類的透明性和復用,現有的類復用但不需要改變。?
????2、目標類和適配器類解耦,提高程序的擴展性。?
????3、在很多業務場景中符合開閉原則。?
????缺點:?
????1、適配器編寫過程需要全面考慮,可能會增加系統的復雜性。?
????2、增加代碼閱讀難度,降低代碼可讀性,過多使用適配器會使系統代碼變得凌亂。
?
九、裝飾者模式(Decorator Pattern)
裝飾者模式(Decorator Pattern)是指在不改變原有對象的基礎之上,將功能附加到對
象上,提供了比繼承更有彈性的替代方案(擴展原有對象的功能),屬于結構型模式。
/***?裝飾者模式(Decorator?Pattern)*?列:煎餅加香腸、加雞蛋*/ public?class?DecoratorPattern?{public?static?abstract?class?BatterCake?{protected?abstract?String?getBatterCake();protected?abstract?int?getPrice();}/***?煎餅類*/public?static?class?BaseBatterCake??extends?BatterCake{@Overrideprotected?String?getBatterCake()?{return?"煎餅";}@Overrideprotected?int?getPrice()?{return?5;}}public?static???abstract?class?BatterCakeDecorator?extends?BatterCake?{//?委派?靜態代理private?BatterCake??batterCake;public?BatterCakeDecorator(BatterCake?batterCake)?{this.batterCake?=?batterCake;}@Overrideprotected?String?getBatterCake()?{return?this.batterCake.getBatterCake();}@Overrideprotected?int?getPrice()?{return?this.batterCake.getPrice();}abstract?void?doSomething();}public?static???class?BatterCakeWithEgg?extends?BatterCakeDecorator?{public?BatterCakeWithEgg(BatterCake?batterCake)?{super(batterCake);}@Overrideprotected?String?getBatterCake()?{return?super.getBatterCake()?+?"雞蛋";}@Overrideprotected?int?getPrice()?{return?super.getPrice()?+?2;}@Overridevoid?doSomething()?{System.out.println("business?....");}}public?static?class?BatterCakeWithEggAndHotDogs?extends??BatterCakeDecorator?{public?BatterCakeWithEggAndHotDogs(BatterCake?batterCake)?{super(batterCake);}@Overrideprotected?String?getBatterCake()?{return?super.getBatterCake()?+?"雞蛋?+?腸";}@Overrideprotected?int?getPrice()?{return?super.getPrice()?+?3;}@Overridevoid?doSomething()?{System.out.println("business?....");}}public?static?void?main(String[]?args)?{BatterCake?cake?=?new?BaseBatterCake();System.out.println(cake.getBatterCake());cake?=?new?BatterCakeWithEgg(cake);System.out.println(cake.getBatterCake());cake?=?new?BatterCakeWithEggAndHotDogs(cake);System.out.println(cake.getBatterCake());} }?
十、觀察者模式(Observe Pattern)
?
觀察者模式定義了對象之間的一對多依賴,讓多個觀察者對象同時監聽一個主體對象,當主體對象發生變化時,它的所有依賴者(觀察者)都會收到通知并更新,屬于行為型模式。
?
/***?觀察者模式(Observe?Pattern)*?場景:學生反饋問題,老師接收*/ public?class?ObservePattern?{public?static?class?Question?{private??String?user;private?String?questionDescribe;public?String?getUser()?{return?user;}public?void?setUser(String?user)?{this.user?=?user;}public?String?getQuestionDescribe()?{return?questionDescribe;}public?void?setQuestionDescribe(String?questionDescribe)?{this.questionDescribe?=?questionDescribe;}}/***??JDK提供觀察者、被觀察者模式*/public?static?class?MesageObserve?extends?Observable?{private?static?MesageObserve?mesageObserve?=?null;private?String?name?=?"學生管理平臺";public?String?getName()?{return?name;}public?static?MesageObserve?getInstance()?{if?(null?==?mesageObserve)?{mesageObserve?=?new?MesageObserve();}return?mesageObserve;}public?void?publishQuestion(Question?question)?{System.out.println("學生:"?+?question.getUser()?+?"在平臺上反饋了一個問題");setChanged();notifyObservers(question);}}public?static?class??Teacher??implements?Observer{private?String?name;public?Teacher(String?name){this.name?=?name;}public?void?update(Observable?o,?Object?arg)?{MesageObserve?gper?=?(MesageObserve)o;Question?question?=?(Question)arg;System.out.println("===============================");System.out.println(name?+?"老師,你好!\n"?+"您收到了一個來自“"?+?gper.getName()?+?"”的提問,希望您解答,問題內容如下:\n"?+question.getQuestionDescribe()?+?"\n"?+"提問者:"?+?question.getUser());}}public?static?void?main(String[]?args)?{MesageObserve?mesageObserve?=?MesageObserve.getInstance();Teacher?zhang?=?new?Teacher("zhang");Teacher?huang?=?new?Teacher("huang");mesageObserve.addObserver(zhang);mesageObserve.addObserver(huang);Question?question?=?new?Question();question.setUser("小明");question.setQuestionDescribe("裝飾者模式怎么使用。。。");mesageObserve.publishQuestion(question);} }十一、責任鏈模式(Chain of Responsibility)
責任鏈模式么一個節點看做是一個對象,每一個節點處理不同業務邏輯,且內部維護下一個節點對象。
/*** 責任鏈模式(Chain of Responsibility)* 列:對參數校驗(非空、登錄、權限)*/ public class ChainOfResponsibility {public static class Params {private String userName;private String token;private String userRule;public String getUserName() {return userName;}########省略部分get set###########}public static abstract class Handler {public Handler chain;public void setNextHandler (Handler handler) {this.chain = handler;}public abstract void doHandler (Params params);}/*** 非空參數驗證Handler*/public static class VolidateHandler extends Handler {@Overridepublic void doHandler(Params params) {System.out.println("參數非空驗證通過。。。");chain.doHandler(params);}}/*** 角色驗證Handler*/public static class RoleHandler extends Handler {@Overridepublic void doHandler(Params params) {System.out.println("角色驗證通過。。。");chain.doHandler(params);}}/*** 權限驗證Handler*/public static class AuthorHandler extends Handler {@Overridepublic void doHandler(Params params) {System.out.println("權限驗證通過。。。");if (null != chain) {chain.doHandler(params);}}}public static class HandlerService {public boolean userLogin(String userName) {Params params = new Params();params.setUserName(userName);params.setToken("token");params.setUserRule("admin");VolidateHandler volidateHandler = new VolidateHandler();RoleHandler roleHandler = new RoleHandler();AuthorHandler authorHandler = new AuthorHandler();volidateHandler.setNextHandler(roleHandler);roleHandler.setNextHandler(authorHandler);volidateHandler.doHandler(params);System.out.println("login success");return true;}}public static void main(String[] args) {new HandlerService().userLogin("zhangsan ");} }文章詳情查看:?http://www.xiaoyuge.com.cn/#/article/detail?articleId=67
總結
以上是生活随笔為你收集整理的荐:Java常见设计模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: PPT2019完成炫酷的切换效果
- 下一篇: 2 年前端面试心路历程(字节跳动、YY、