單例模型
餓漢式
靜態方法創建對象
public class Singleton {// 私有構造方法private Singleton(){}private static Singleton instance = new Singleton();// 提供一個外界獲取的方法public static Singleton getInstance(){return instance;}
}
靜態代碼塊創建對象
public class Singleton {private Singleton(){}private static Singleton instance;static {instance = new Singleton();}public static Singleton getInstance(){return instance;}
}
懶漢式
synchronized關鍵字
public class Singleton {private Singleton(){}private static Singleton instance;public static Singleton getInstance(){if(instance == null){instance = new Singleton();}return instance;}
}
【
問題
】上邊的代碼是存在線程不安全的情況的,當線程1進來,判讀instance==null成立,準備創建對象;但是線程1還沒創建對象完畢時,線程2來了,線程2也判斷成立,也去創建對象,此時就會創建兩個不同的對象。
【解決
】:給getInstance方法上添加synchronized關鍵字
public class Singleton {private Singleton(){}private static Singleton instance;public static synchronized Singleton getInstance(){if(instance == null){instance = new Singleton(); // 下邊簡稱:“寫操作”}return instance; // 下邊簡稱:“讀操作”}
}
雙重檢查鎖
【問題
】:對于getInstance()方法,其實大部分的操作都是讀操作,讀操作是線程安全的,如果直接給getInstance方法上加鎖,其實會造成大量的線程等待。
【解決
】:調整加鎖的時機,雙重檢查鎖
public class Singleton {private Singleton(){}private static volatile Singleton instance; // volatile:保證指令的有序性和可見性public static Singleton getInstance(){// 第一次判斷,如果instance的值不為null,不需要搶占鎖,直接返回對象if(instance == null){synchronized (Singleton.class){// 第二次判斷if(instance == null){instance = new Singleton();}}}return instance;}
}
靜態內部類
public class Singleton {private Singleton(){}// 定義一個靜態內部類private static class SingletonHolder{// 在內部類中聲明并初始化外部類的對象private static final Singleton INSTANCE = new Singleton();}public static Singleton getInstance(){return SingletonHolder.INSTANCE;}
}
JVM加載外部類的過程是不會加載靜態內部類的,只有內部類的屬性、方法被調用時才會加載并初始化靜態屬性。靜態屬性被static修飾,所以只會被實例化一次。
枚舉類
枚舉類也是線程安全的,只會被加載一次,也是所有單例實現中唯一一種不會被破壞的單例模式
public enum Singleton {INSTANCE
}
枚舉方式是屬于餓漢式的方式,在不考慮浪費內存空間的情況下,首選枚舉方式
存在的問題
【問題
】:破壞單例模式(讓單例模式可以創建多個對象,枚舉方式除外)
通過序列化和反序列化破壞單例模式
public class Client {public static void main(String[] args) throws Exception {writeObject2File();readObjectFromFile(); // Singleton@27bc2616readObjectFromFile(); // Singleton@3941a79c}// 從文件中讀取對象private static void readObjectFromFile() throws Exception {// 1. 創建輸入流對象ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\Desktop\\a.txt"));// 2. 讀取對象Singleton instance = (Singleton) ois.readObject();System.out.println(instance);// 3. 釋放資源ois.close();}// 從文件中寫對象public static void writeObject2File() throws Exception {// 1. 獲取Singleton對象Singleton instance = Singleton.getInstance();// 2. 將對象寫入文件ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\Desktop\\a.txt"));oos.writeObject(instance);// 3. 釋放資源oos.close();}
}
上面的代碼生成的兩個對象不是同一個對象,破壞了單例模式
通過反射破壞單例模式
public class Client {public static void main(String[] args) throws Exception {// 1. 獲取Singleton的字節碼對象Class clazz = Singleton.class;// 2. 獲取無參構造方法對象Constructor cons = clazz.getDeclaredConstructor();// 3. 取消訪問檢查(暴力反射)cons.setAccessible(true);// 4. 創建對象Singleton s1 = (Singleton) cons.newInstance();Singleton s2 = (Singleton) cons.newInstance();System.out.println(s1 == s2); // false - 破壞單例模式}
}
上邊代碼返回的是false,說明s1和s2不是同一個對象,破壞了單例模式
問題解決
序列化、反序列化破壞單例模式解決方法
在Singleton類種添加readResolve()方法,在反序列化時被反射調用。如果定義了這個方法,就返回這個方法的值,如果沒有定義,則返回new出來的對象。
public class Singleton implements Serializable {private Singleton(){}// 定義一個靜態內部類private static class SingletonHolder{// 在內部類中聲明并初始化外部類的對象private static final Singleton INSTANCE = new Singleton();}public static Singleton getInstance(){return SingletonHolder.INSTANCE;}// 當進行反序列化時,自動調用該方法,將該方法的返回值直接返回public Object readResolve(){return SingletonHolder.INSTANCE;}
}
反射方式破壞單例模式解決方法
其實反射破壞的原理是:通過反射獲取Singleton的私有構造方法,然后通過這個私有的構造方法去創建對象。
因此我們只需要在構造方法里添加一個判斷即可
public class Singleton implements Serializable {private static boolean flag = false;private Singleton(){synchronized (Singleton.class){if(flag) {throw new RuntimeException("不能創建多個對象");}flag = true;}}// 定義一個靜態內部類private static class SingletonHolder{// 在內部類中聲明并初始化外部類的對象private static final Singleton INSTANCE = new Singleton();}public static Singleton getInstance(){return SingletonHolder.INSTANCE;}
}
JDK源碼解析 - Runtime類
餓漢式:
工廠模式
引例:點咖啡
現在有美式咖啡、拿鐵咖啡,顧客可以選擇咖啡的種類,咖啡都需要進行加糖加奶。
原本的寫法:
咖啡類:
public abstract class Coffee {public abstract String getName();// 加糖public void addsugar() {System.out.println("加糖");}// 加奶public void addmilk() {System.out.println("加奶");}
}
美式咖啡:
public class AmericanCoffee extends Coffee {@Overridepublic String getName() {return "美式咖啡";}
}
拿鐵咖啡:
public class LatteCoffee extends Coffee {@Overridepublic String getName() {return "拿鐵咖啡";}
}
咖啡店:
public class CoffeeStore {public Coffee orderCoffee(String coffeeType) {// 聲明Coffee類型的變量,根據不同的類型創建不同的子類對象Coffee coffee = null;if("american".equals(coffeeType)) {coffee = new AmericanCoffee();}else if("latte".equals(coffeeType)) {coffee = new LatteCoffee();}else {throw new RuntimeException("對不起的,您所點的咖啡沒有");}// 加配料coffee.addmilk();coffee.addsugar();return coffee;}
}
測試方法:
public class Client {public static void main(String[] args) {// 創建咖啡店類CoffeeStore store = new CoffeeStore();// 點咖啡Coffee coffee = store.orderCoffee("latte");System.out.println(coffee.getName());}
}
【存在問題
】:如果需要更換對象,那么所有new對象的地方都要修改一遍,這就違背了軟件設計的開閉原則。
【解決
】:工廠模式
簡單工廠模式
角色:
- 抽象產品:定義了產品的規范(咖啡類)
- 具體產品:是現貨集成抽象產品的子類(美式咖啡、拿鐵咖啡)
- 具體工廠:提供了創建產品的方法,調用者通過該方法來獲取產品
簡單咖啡工廠類,用來生產咖啡:
public class SimpleCoffeeFactory {public Coffee createCoffee(String coffeeType) {// 聲明Coffee類型的變量,根據不同的類型創建不同的子類對象Coffee coffee = null;if("american".equals(coffeeType)) {coffee = new AmericanCoffee();}else if("latte".equals(coffeeType)) {coffee = new LatteCoffee();}else {throw new RuntimeException("對不起的,您所點的咖啡沒有");}return coffee;}
}
咖啡店:
public class CoffeeStore {public Coffee orderCoffee(String coffeeType) {SimpleCoffeeFactory factory = new SimpleCoffeeFactory();// 調用生產咖啡的方法Coffee coffee = factory.createCoffee(coffeeType);// 加配料coffee.addmilk();coffee.addsugar();return coffee;}
}
解除了CoffeeStore和具體的咖啡的耦合
【優勢
】:工廠類的客戶端可能有很多,這樣只需要去修改SimpleCoffeeFactory的代碼,可以省去其他的修改操作。
【劣勢
】:如果要再加新的品種的咖啡,就必須要修改SimpleCoffeeFactory的代碼,這違反了開閉原則。
工廠方法模式
角色:
- 抽象產品:定義了產品的規范(咖啡類)
- 具體產品:是現貨集成抽象產品的子類(美式咖啡、拿鐵咖啡)
- 抽象工廠:提供創建產品的接口,調用者通過訪問它具體工廠的工廠方法來創建產品
- 具體工廠:提供了創建產品的方法,調用者通過該方法來獲取產品
抽象工廠:
public interface CoffeeFactory {// 創建咖啡對象的方法Coffee createCoffee();
}
具體工廠:
- 拿鐵咖啡工廠對象 - 用來生產拿鐵咖啡
public class LatteCoffeeFactory implements CoffeeFactory {@Overridepublic Coffee createCoffee() {return new LatteCoffee();}
}
- 美式咖啡工廠對象 - 用來生產美式咖啡
public class AmericanCoffeeFactory implements CoffeeFactory {@Overridepublic Coffee createCoffee() {return new AmericanCoffee();}
}
咖啡店:
public class CoffeeStore {private CoffeeFactory factory;public void setFactory(CoffeeFactory factory) {this.factory = factory;}// 點咖啡public Coffee orderCoffee() {// 創建咖啡Coffee coffee = factory.createCoffee();// 加配料coffee.addmilk();coffee.addmilk();return coffee;}
}
測試方法:
public class Client {public static void main(String[] args) {// 創建咖啡店類CoffeeStore store = new CoffeeStore();store.setFactory(new AmericanCoffeeFactory()); // 生產美式咖啡// 點咖啡Coffee coffee = store.orderCoffee();System.out.println(coffee.getName());}
}
【
優勢
】:用戶只要知道具體工程的類名就可以得到產品;系統增加新的產品只需要新增具體產品類和對應的具體工廠類即可。
【劣勢
】:每增加一個產品就需要增加一個具體產品類和具體工廠類, 增加了系統的復雜度
抽象工廠模式
抽象工廠模式和工廠方法模式的區別:
- 工廠方法模式:只生產一個等級的產品
- 抽象工廠模式:可以創建多個不同等級的產品
【需求變更
】:現在咖啡店不僅需要生產咖啡,還需要生產甜品
甜品抽象類:
public abstract class Dessert {abstract void show();
}
提拉米蘇類:
public class Trimisu extends Dessert{@Overridevoid show() {System.out.println("提拉米蘇");}
}
抹茶慕斯類:
public class MatchaMousse extends Dessert{@Overridevoid show() {System.out.println("抹茶慕斯");}
}
甜品抽象工廠:
public interface DessertFactory {// 生產咖啡的功能Coffee createCoffee();// 生產甜品的功能Dessert createDessert();
}
意大利風味甜品工廠(生產拿鐵咖啡和提拉米蘇甜品):
public class ItaltyDessertFactory implements DessertFactory{ // 意大利風味甜品工廠(生產拿鐵咖啡和提拉米蘇甜品)@Overridepublic Coffee createCoffee() {return new LatteCoffee(); // 拿鐵咖啡}@Overridepublic Dessert createDessert() {return new Trimisu(); // 提拉米蘇}
}
美式咖啡的甜品工廠(生產美式咖啡和抹茶慕斯):
public class AmericanDessertFactory implements DessertFactory{ // 美式咖啡的甜品工廠 - 生產美式咖啡和抹茶慕斯@Overridepublic Coffee createCoffee() {return new AmericanCoffee(); // 美式咖啡}@Overridepublic Dessert createDessert() {return new MatchaMousse(); // 抹茶慕斯}
}
測試類:
public class Client {public static void main(String[] args) {// 創建意大利風味的工廠ItaltyDessertFactory it = new ItaltyDessertFactory();Coffee coffee = it.createCoffee(); // 拿鐵咖啡Dessert dessert = it.createDessert(); // 提拉米蘇System.out.println(coffee.getName());dessert.show();}
}
如果要加一個產品族,只需要再加一個對應的工廠類,不需要修改其他類
【優點
】:客戶端只能使用同一個產品族中的對象
【缺點
】:產品族種需要新增一個新的產品,所有的工廠類都需要進行修改。
適用場景:
- 需要創建的對象是一系列相互關聯或相互依賴的產品族(電器工廠中的電視機、洗衣機、空調)
- 系統種有多個產品族每次只用其中一種產品(有人只喜歡穿一個品牌的衣服和褲子)
- 系統提供了產品的類庫,且所有產品的接口相同,客戶端不依賴產品實例的創建細節和內部結構
- 如:搜狗輸入法換皮膚(一套一起換)
模式擴展(Spring框架底層)
bean.properties文件:
american = com.itheima.pattern02factory.factory_04_config.AmericanCoffee
latte = com.itheima.pattern02factory.factory_04_config.LatteCoffee
工廠類:
public class CoffeeFactory {// 1. 定義容器對象存儲咖啡對象private static HashMap<String, Coffee> map = new HashMap<>();// 2. 加載配置文件,并創建該配置文件里類的對象并進行存儲(只需要加載一次)static {// 2.1 創建Properties對象Properties p = new Properties();// 2.2 調用p對象中的load方法進行配置文件的加載InputStream is = CoffeeFactory.class.getClassLoader().getResourceAsStream("bean.properties");try {p.load(is);// 2.3 從p集合中獲取全類名并創建對象Set<Object> keys = p.keySet();for (Object key : keys) {String className = p.getProperty((String) key);// 2.4 通過反射技術創建對象Class clazz = Class.forName(className);Coffee coffee = (Coffee) clazz.newInstance();// 2.5 將名稱和對象存儲到容器中map.put((String) key, coffee);}} catch (Exception e) {throw new RuntimeException(e);}}// 根據名稱獲取對象public static Coffee createCoffee(String name) {return map.get(name);}
}
JDK源碼解析 - Collection.iterator()
Collection是抽象工廠;ArrayList是具體工廠
【補】:DateForamt類中的getInstance()、Calendar類中的getInstance()也是工廠模式
原型模式
用一個已經創建的對象作為原型,復制這個原型對象來創建一個和原型對象相同的新對象。
包含的角色:
- 抽象原型類:規定了具體原型對象必須實現的clone()方法
- 具體原型類:實現抽象原型類中的clone()方法,他是可以被復制的對象
淺克隆:創建一個新對象,新對象的屬性和原來對象完全相同。對于非基本類型屬性,克隆對象和源對象指向的是同一塊內存空間
深克隆:創建一個新對象,屬性中的引用的其他對象也會被克隆,不會指向原有對象地址。
引例1:克隆對象
具體原型對象
:
public class Relizetype implements Cloneable { // 必須實現Cloneable接口:否則調用clone()會拋出CloneNotSupportedExceptionpublic Relizetype() {System.out.println("具體的原型類創建成功");}@Overridepublic Relizetype clone() throws CloneNotSupportedException { // clone()方法是在Object類里的System.out.println("具體原型復制成功");return (Relizetype) super.clone();}
}
測試類
:
public class Client {public static void main(String[] args) throws CloneNotSupportedException {// 創建原型對象Relizetype relizetype = new Relizetype();// 調用原型類中的clone()方法進行對象的克隆Relizetype clone = relizetype.clone();System.out.println(relizetype == clone); // false}
}
必須實現Cloneable接口:否則調用clone()會拋出CloneNotSupportedException。
重寫clone()方法:通常需將其改為public訪問權限,并返回具體類型。
clone() 方法創建了一個新的對象,而不是返回原對象的引用,因為java的Object.clone()方法是一個native方法,不會調用構造方法,而是直接分配內存并復制數據
引例2.1:三好學生獎狀分發(淺克隆)
獎狀類:
@Data
public class Citation implements Cloneable {// 三好學生上的姓名private String name;@Overridepublic Citation clone() throws CloneNotSupportedException {return (Citation) super.clone();}public void show() {System.out.println(name + "同學被評為三好學生");}
}
測試類:
public class CitationTest {public static void main(String[] args) throws CloneNotSupportedException {// 1. 創建原型對象Citation citation = new Citation();// 2. 克隆獎狀對象Citation citation1 = citation.clone();citation.setName("張三");citation1.setName("李四");citation.show(); // 張三同學被評為三好學生citation1.show(); // 李四同學被評為三好學生}
}
使用場景
- 對象的創建非常復雜,可以使用原型模式快捷創建對象(原型對象的所屬類必須實現clone()方法)
- 性能和安全的要求比較高
引例2.2:三好學生獎狀分發(深克隆)
學生類:
@Data
@Accessors(chain = true)
public class Student {// 學生姓名private String name;
}
獎狀類:
@Data
public class Citation implements Cloneable {private Student student;@Overridepublic Citation clone() throws CloneNotSupportedException {return (Citation) super.clone();}public void show() {System.out.println(student.getName() + "同學被評為三好學生");}
}
測試類:
public class CitationTest {public static void main(String[] args) throws CloneNotSupportedException {// 1. 創建原型對象Citation citation = new Citation();Student stu = new Student().setName("張三");citation.setStudent(stu);// 2. 克隆獎狀對象Citation citation1 = citation.clone();Student stu1 = citation1.getStudent();stu1.setName("李四");citation.show(); // 李四同學被評為三好學生citation1.show(); // 李四同學被評為三好學生}
}
【
問題
】由上邊測試結果可知,修改了citation1成員變量的值的同時,也修改了citation對象的值
【產生原因
】這是因為stu和stu1此時是一個對象(這就是淺克隆),此時將stu1的屬性改成“李四”,導致stu的屬性也變成“李四”
【修改
】:把淺克隆變成深克隆(使用序列化和反序列化實現)
public class CitationTest1 {public static void main(String[] args) throws Exception {// 1. 創建原型對象Citation citation = new Citation();Student stu = new Student().setName("張三");citation.setStudent(stu);// 把對象寫入文件中ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("a.txt")); // 對象輸出流對象oos.writeObject(citation); // 寫對象oos.close(); // 釋放資源// 從文件中讀取對象 (2. 克隆獎狀對象)ObjectInputStream ois = new ObjectInputStream(new FileInputStream("a.txt"));Citation citation1 = (Citation) ois.readObject();citation1.getStudent().setName("李四");ois.close();citation.show(); // 張三同學被評為三好學生citation1.show(); // 李四同學被評為三好學生}
}
Citation類和Student類都必須實現Serializable接口,否則會拋NotSerializableException異常
【修改
】:把淺克隆變成深克隆(在重寫clone()方法的時候調用屬性的clone()方法)
獎狀類:
@Data
public class Citation implements Cloneable, Serializable {private Student student;@Overridepublic Citation clone() throws CloneNotSupportedException {Citation clone = (Citation) super.clone();clone.setStudent(student.clone()); // 深拷貝return clone;}public void show() {System.out.println(student.getName() + "同學被評為三好學生");}
}
學生類:
@Data
@Accessors(chain = true)
public class Student implements Serializable, Cloneable {// 學生姓名private String name;@Overridepublic Student clone() throws CloneNotSupportedException {return (Student) super.clone();}
}
測試類:
public class CitationTest {public static void main(String[] args) throws CloneNotSupportedException {// 1. 創建原型對象Citation citation = new Citation();Student stu = new Student().setName("張三");citation.setStudent(stu);// 2. 克隆獎狀對象Citation citation1 = citation.clone();citation1.getStudent().setName("李四");citation.show(); // 張三同學被評為三好學生citation1.show(); // 李四同學被評為三好學生}
}
建造者模式
將復雜對象的構建和表示分離,使同樣的構建過程可以創建不同的表示。
建造者建造的產品一般需要有較多的共同點,組成部分需要相似(如果產品之間差異比較大,不適合使用建造者模式)
- 產品類(Product):要創建的復雜對象
- 抽象建造者類(Builder):這個接口規定要實現復雜對象那部分的創建,不涉及具體的對象創建
- 具體建造者類(ConcreteBuilder):實現Builder接口,完成復雜產品的各個部件的具體創建方法(強調裝配的過程)
- 指揮者類(Director):調用具體建造者來創建復雜對象的各個部分,不涉及具體產品信息,只保證對象各個部分完整創建或按照某種順序創建
引例:創建共享單車
【需求
】:自行車包含了車架、車座等組件的生產;車架又有碳纖維,鋁合金等材質;車座有橡膠、真皮的材質
Bike類:產品類(車架、車座組件)
Builder:抽象建造者(MobikeBuilder、OfoBuilder是具體的建造者)
Director:指揮者
產品類:
@Data
public class Bike {/*車架*/private String frame;/*車座*/private String seat;
}
抽象構建者:
public abstract class Builder {/*Bike對象*/protected Bike bike = new Bike(); // 目前還沒有組裝組件(指揮者做)/*構建車架*/public abstract void buildFrame();/*構建車座*/public abstract void buildSeat();/*構建自行車*/public abstract Bike createBike();
}
具體構建者1(摩拜單車):
public class MobileBuilder extends Builder{@Overridepublic void buildFrame() {bike.setFrame("碳纖維車架");}@Overridepublic void buildSeat() {bike.setSeat("真皮車座");}@Overridepublic Bike createBike() {return bike;}
}
具體構建者2(ofo單車):
public class OfoBuilder extends Builder{@Overridepublic void buildFrame() {bike.setFrame("鋁合金車架");}@Overridepublic void buildSeat() {bike.setSeat("橡膠車座");}@Overridepublic Bike createBike() {return bike;}
}
指揮者類:
public class Director {private Builder builder;public Director(Builder builder) {this.builder = builder;}/*組裝自行車*/public Bike construct() {builder.buildFrame();builder.buildSeat();return builder.createBike();}
}
測試類:
public class Client {public static void main(String[] args) {// 1. 創建指揮者對象Director director = new Director(new MobileBuilder());// 2. 讓指揮者進行自行車的組裝Bike bike = director.construct();System.out.println(bike.getFrame());System.out.println(bike.getSeat());}
}
Director指揮者類在建造者模式中很重要,是由指揮者類來指導具體的建造者應該如何構建產品,控制調用的先后順序,向調用者返回完整的產品類。
【改進
】:指揮者類也可以和抽象建造者進行結合:
public abstract class Builder {/*Bike對象*/protected Bike bike = new Bike(); // 目前還沒有組裝組件(指揮者做)/*構建車架*/public abstract void buildFrame();/*構建車座*/public abstract void buildSeat();/*構建自行車*/public abstract Bike createBike();/*組裝自行車*/public Bike construct() {this.buildFrame();this.buildSeat();return this.createBike();}
}
這樣做雖然可以不用寫指揮者類,但是也加重了建造者類的職責,也不符合單一職責原則,如果construct()過于復雜,還是建議封裝到Director中。
模式擴展
當一個類的構造方法需要傳入很多參數,如果創建這個類的實例,代碼的可讀性就會很差,就可以使用建造者模式進行重構。
手機類:
@Data
public class Phone {private String cpu;private String screen;private String memory;private String mainboard;/*私有構造方法*/private Phone(Builder builder) {this.cpu = builder.cpu;this.screen = builder.screen;this.memory = builder.memory;this.mainboard = builder.mainboard;}public static final class Builder {private String cpu;private String screen;private String memory;private String mainboard;public Builder cpu(String cpu) {this.cpu = cpu;return this; // 為了鏈式編程}public Builder screen(String screen) {this.screen = screen;return this;}public Builder memory(String memory) {this.memory = memory;return this;}public Builder mainboard(String mainboard) {this.mainboard = mainboard;return this;}/*使用構建者創建Phone對象*/public Phone build() {return new Phone(this);}}
}
測試類:
public class Client {public static void main(String[] args) {/*創建手機對象 - 通過構建者對象獲取手機對象*/Phone phone = new Phone.Builder().cpu("intel").screen("三星").memory("金士頓內存條").mainboard("華碩").build();System.out.println(phone); // Phone(cpu=intel, screen=三星, memory=金士頓內存條, mainboard=華碩)}
}
將構建的順序交給客戶,這個相當于lombok里的
@Builder
注解
構建者模式對比
工廠方法模式 vs 建造者模式
- 工廠方法模式:整體對象的創建方式
- 建造者模式:部件構建的過程
抽象工廠模式 vs 建造者模式
- 抽象工廠模式:實現對產品家族的創建,不需要關心建造過程,只關心什么產品由什么工廠生產
- 建造者模式:按照指定的藍圖建造產品,通過組裝零件而產生一個新產品
抽象工廠模式:汽車配件生產工廠(生產一個產品族的產品)
建造者模式:騎車組裝工廠(通過對配件的組裝可以返回一個完整的騎車)