如何在Java中实现与单应性方法的接口?

时间:2013-05-11 06:13:50

标签: java inheritance multiple-inheritance diamond-problem

在英语中,同形异义词对是两个具有相同拼写但含义不同的词。

在软件工程中,一对同形方法是两种具有相同名称但要求不同的方法。让我们看一个人为的例子,让问题尽可能清楚:

interface I1 { 
    /** return 1 */ 
    int f()
}
interface I2 {
    /** return 2*/
    int f()
}
interface I12 extends I1, I2 {}

如何实施I12? C#has a way要做到这一点,但Java没有。所以唯一的方法是破解。如何使用反射/字节码技巧/等最可靠(即它不一定是一个完美的解决方案,我只想要一个最好的解决方案)?


请注意,我无法合法反向工程的一些现有的闭源大量遗留代码需要I12类型的参数,并将I12两者委托给具有I1的代码参数和以I2作为参数的代码。所以基本上我需要创建一个I12的实例,它知道什么时候应该充当I1,什么时候应该充当I2,我相信可以通过looking at the bytecode at runtime完成直接来电者。我们可以假设调用者没有使用反射,因为这是简单的代码。问题是I12的作者没想到Java会从两个接口合并f,所以现在我必须想出最好的解决问题的方法。什么都没有调用I12.f(显然如果作者写了一些实际调用I12.f的代码,他会在出售之前注意到这个问题。)

请注意,我实际上是在寻找这个问题的答案,而不是如何重构我无法改变的代码。我正在寻找可行的最佳启发式或者如果存在的话,可以找到精确的解决方案。请参阅Gray的答案以获得有效示例(我确信有更强大的解决方案)。


Here是两个界面中单应方法问题如何发生的具体例子。这是另一个具体的例子:

我有以下6个简单的类/接口。它类似于剧院周围的商业和在其中表演的艺术家。为了简单起见,我们假设它们都是由不同的人创建的。

Set代表一个集合,如集理论中所述:

interface Set {
    /** Complements this set,
        i.e: all elements in the set are removed,
        and all other elements in the universe are added. */
    public void complement();
    /** Remove an arbitrary element from the set */
    public void remove();
    public boolean empty();
}

HRDepartment使用Set来代表员工。它使用复杂的流程来解码雇用/解雇的员工:

import java.util.Random;
class HRDepartment {
    private Random random = new Random();
    private Set employees;

    public HRDepartment(Set employees) {
        this.employees = employees;
    }

    public void doHiringAndLayingoffProcess() {
        if (random.nextBoolean())
            employees.complement();
        else
            employees.remove();
        if (employees.empty())
            employees.complement();
    }
}

Set员工的范围可能是申请雇主的员工。因此,当在该集合上调用complement时,将触发所有现有员工,并且之前应用的所有其他员工都将被雇用。

Artist代表艺术家,例如音乐家或演员。艺术家有自我。当别人赞美他时,这种自我会增加:

interface Artist {
    /** Complements the artist. Increases ego. */
    public void complement();
    public int getEgo();
}

Theater执行Artist,这可能会导致Artist得到补充。剧院的观众可以在表演之间评判艺术家。表演者的自我越高,观众越有可能喜欢Artist,但如果自我超越某一点,艺术家将受到观众的负面看法:

import java.util.Random;
public class Theater {
    private Artist artist;
    private Random random = new Random();

    public Theater(Artist artist) {
        this.artist = artist;
    }
    public void perform() {
        if (random.nextBoolean())
            artist.complement();
    }
    public boolean judge() {
        int ego = artist.getEgo();
        if (ego > 10)
            return false;
        return (ego - random.nextInt(15) > 0);
    }
}

ArtistSet只是ArtistSet

/** A set of associated artists, e.g: a band. */
interface ArtistSet extends Set, Artist {
}

TheaterManager运行节目。如果剧院的观众对艺术家负面评价,剧院会与人力资源部门进行对话,人力资源部门将反过来解雇艺术家,雇用新的艺术家等等:

class TheaterManager {
    private Theater theater;
    private HRDepartment hr;

    public TheaterManager(ArtistSet artists) {
        this.theater = new Theater(artists);
        this.hr = new HRDepartment(artists);
    }

    public void runShow() {
        theater.perform();
        if (!theater.judge()) {
            hr.doHiringAndLayingoffProcess();
        }
    }
}

一旦您尝试实现ArtistSet,问题就会变得清晰:两个超接口都指定complement应该执行其他操作,因此您必须在其中实现两个具有相同签名的complement方法不知何故,同一个班级。 Artist.complementSet.complement的同形异义词。

7 个答案:

答案 0 :(得分:4)

新想法,有点凌乱......

public class MyArtistSet implements ArtistSet {

    public void complement() {
        StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();

        // the last element in stackTraceElements is the least recent method invocation
        // so we want the one near the top, probably index 1, but you might have to play
        // with it to figure it out: could do something like this

        boolean callCameFromHR = false;
        boolean callCameFromTheatre = false;

        for(int i = 0; i < 3; i++) {
           if(stackTraceElements[i].getClassName().contains("Theatre")) {
               callCameFromTheatre = true;
           }
           if(stackTraceElements[i].getClassName().contains("HRDepartment")) {
               callCameFromHR = true;
           }
        }

        if(callCameFromHR && callCameFromTheatre) {
            // problem
        }
        else if(callCameFromHR) {
            // respond one way
        }
        else if(callCameFromTheatre) {
            // respond another way
        }
        else {
            // it didn't come from either
        }
    }
}

答案 1 :(得分:3)

如何解决您的具体案例

  

ArtistSet只是一个艺术家和一个集合:

 /** A set of associated artists, e.g: a band. */
 interface ArtistSet extends Set, Artist { }

从OO的角度来看,这不是一个有用的声明。艺术家是一种名词,一种定义了属性和动作(方法)的“东西”。 集合是事物的集合 - 独特元素的集合。相反,尝试:

ArtistSet只是一组艺术家。

 /** A set of associated artists, e.g: a band. */
 interface ArtistSet extends Set<Artist> { };

然后,对于您的特定情况,同音词方法是在从不在一种类型中组合的接口上,因此您没有冲突并且可以编程...

此外,您不需要声明ArtistSet,因为您实际上并未使用任何新声明扩展Set。您只是实例化一个类型参数,因此您可以用Set<Artist>替换所有用法。

如何解决更常见的案例

对于这种冲突,方法名称甚至不需要在英语意义上是单应性的 - 它们可以是具有相同英语含义的相同单词,在java中的不同上下文中使用。如果您希望将两个接口应用于某个类型但它们包含具有冲突语义/处理定义的相同声明(例如方法签名),则会发生冲突。

Java不允许您实现您请求的行为 - 您必须有另一种解决方法。 Java不允许类为来自多个不同的方法签名提供多个实现接口(多次实现相同的方法,并使用某种形式的限定/别名/注释来区分)。见Java overriding two interfaces, clash of method namesJava - Method name collision in interface implementation

E.g。如果您有以下

 interface TV {
     void switchOn();
     void switchOff();
     void changeChannel(int ChannelNumber);
 }

 interface Video {
     void switchOn();
     void switchOff();
     void eject();
     void play();
     void stop();
 }

然后,如果你有一个同时具有这两个对象的对象,你可以将它们组合在一个新的界面中(可选)或输入:

interface TVVideo {
     TV getTv();
     Video getVideo();
}


class TVVideoImpl implements TVVideo {
     TV tv;
     Video video;

     public TVVideoImpl() {
         tv = new SomeTVImpl(....);
         video = new SomeVideoImpl(....);
     }

     TV getTv() { return tv };
     Video getVideo() { return video };
}

答案 2 :(得分:3)

尽管Gray Kemmey的勇敢尝试,我会说问题,因为你已经说过它是不可解决的。作为ArtistSet的一般规则,您无法知道调用它的代码是期望Artist还是Set

此外,即使你可以,根据你对其他各种答案的评论,你实际上还要求将ArtistSet传递给供应商提供的函数,这意味着函数没有给编译器或人类任何了解它的期望。对于任何技术上正确的答案,你完全没有运气。

作为完成工作的实际编程问题,我将执行以下操作(按此顺序):

  1. 向任何创建需要ArtistSet的界面的人以及生成ArtistSet界面的人提交错误报告。
  2. 向供应商提供支持请求,提供需要ArtistSet的功能,并询问他们对complement()的行为的期望。
  3. 实施complement()函数以抛出异常。
  4. public class Sybil implements ArtistSet {
      public void complement() { 
        throw new UnsupportedOperationException('What am I supposed to do'); 
      }
      ...
    }
    

    因为说真的,你不知道该怎么做。当这样调用时,做什么是正确的做法(你怎么知道)?

    class TalentAgent {
        public void pr(ArtistSet artistsSet) {
          artistSet.complement();
        }
    }
    

    通过抛出异常,您有机会获得堆栈跟踪,从而为您提供关于调用者期望的两种行为中的哪一种的线索。幸运的是,没有人会调用该功能,这就是为什么供应商在运输代码方面遇到了这个问题。虽然运气较少但仍有一些,他们处理异常。如果不是这样,那么,至少现在你将有一个堆栈跟踪,你可以查看以确定调用者真正期待的内容并可能实现它(虽然我不情愿地想到这样一个错误,我已经解释了我是怎么回事会做in this other answer)。

    BTW,对于其余的实现,我会将所有内容委托给通过构造函数传入的实际ArtistSet对象,以便以后可以轻松拆分。

答案 3 :(得分:1)

如何实现一个具有两个具有单应方法的超接口的类?

在Java中,具有两个具有单应方法的超接口的类被认为仅具有该方法的一个实现。 (见Java Language Specification section 8.4.8)。这允许类方便地从多个接口继承,这些接口都实现相同的其他接口,并且只实现一次该功能。这也简化了语言,因为这样就不需要语法和方法调度支持来区分基于它们来自哪个界面的单应方法。

因此,实现具有两个具有单应方法的超接口的类的正确方法是提供满足两个超接口的契约的单个方法。

C#有办法做到这一点。如何在Java中完成?这个没有构造吗?

C#定义的接口与Java不同,因此具有Java不具备的功能。

在Java中,语言结构被定义为表示所有接口都获得相同方法的单一实现。没有Java语言构造用于基于对象的编译时类创建多重继承的接口函数的替代行为。这是Java语言设计者的有意识选择。

如果没有,如何最可靠地使用反射/字节码技巧/等?

&#34;它&#34;不能用反射/字节码技巧来完成,因为决定选择哪个接口版本的单应方法所需的信息不一定存在于Java源代码中。鉴于:

interface I1 { 
    // return ASCII character code of first character of String s 
    int f(String s); // f("Hello") returns 72
}
interface I2 {
    // return number of characters in String s 
    int f(String s);  // f("Hello") returns 5
}

interface I12 extends I1, I2 {}

public class C {
  public static int f1(I1 i, String s) { return i.f(s); }  // f1( i, "Hi") == 72
  public static int f2(I2 i, String s) { return i.f(s); }  // f2( i, "Hi") == 2
  public static int f12(I12 i, String s) { return i.f(s);} // f12(i, "Hi") == ???
}

根据Java语言规范,实现I12的类必须以C.f1()C.f2()C.f12()返回完全相同的结果的方式执行此操作。用相同的参数调用。如果C.f12(i, "Hello")有时会返回72并且有时会根据调用C.f12()的方式返回5,那么这将是程序中的一个严重错误并且违反了语言规范。

此外,如果类C的作者期望f12()中出现某种一致行为,则C类中没有字节码或其他信息表明它是否应该是{{I1.f(s)的行为。 1}}或I2.f(s)。如果C.f12()的作者考虑到C.f("Hello")应该返回5或72,则无法通过查看代码来判断。

很好,所以我一般不能使用字节码技巧为单调函数提供不同的行为,但我真的有一个像我的示例类TheaterManager的类。我该怎么做才能实现ArtistSet.complement()

您提出的实际问题实际答案是创建自己的TheaterManager替代实施,不需要ArtistSet。您无需更改库的实现,您需要编写自己的库。

您引用的actual answer other example question基本上是&#34;将I12.f()委托给I2.f()&#34;因为没有接收I12对象的函数继续将该对象传递给期望I1对象的函数。

Stack Overflow仅用于普遍感兴趣的问题和答案

在此拒绝提问的其中一个原因是,&#34;它只与一般不适用于全球互联网受众的极其狭窄的情况有关。&#34; 因为我们想要提供帮助,处理这些狭隘问题的首选方法是修改问题以便更广泛地适用。对于这个问题,我采取了回答广泛适用的问题版本的方法,而不是实际编辑问题,以消除使其成为独特的问题。

在商业编程的现实世界中,任何具有破坏界面的Java库(如I12)都不会累积甚至数十个商业客户端,除非可以通过以下方式之一实现I12.f()来使用它们:

  • 委托给I1.f()
  • 委托给I2.f()
  • 什么都不做
  • 抛出异常
  • 根据I12对象的某些成员的值,在每次调用的基础上选择上述策略之一

如果成千上万甚至只有少数公司在Java中使用此库的这一部分,那么您可以放心,他们已经使用了其中一种解决方案。如果图书馆甚至没有被少数几家公司使用,那么Stack Overflow的问题就太窄了。

好的,TheaterManager过于简单了。在实际情况下,我很难替换那个课程而且我不喜欢你所概述的任何实际解决方案。我不能用花哨的JVM技巧解决这个问题吗?

这取决于你想要修复的内容。如果要通过将所有调用映射到I12.f()来修复特定库,然后解析堆栈以确定调用者并根据该行为选择行为。您可以通过Thread.currentThread().getStackTrace()访问堆栈。

如果您遇到呼叫者,则无法识别您可能很难确定他们想要的版本。例如,您可以从泛型中调用(就像您给出的other specific example中的实际情况一样),例如:

public class TalentAgent<T extends Artist> {
  public static void butterUp(List<T> people) {
    for (T a: people) {
      a.complement()
    }
  }
}

在Java中,generics are implemented as erasures,意味着在编译时抛弃所有类型信息。 TalentAgent<Artist>TalentAgent<Set>之间没有类别或方法签名差异,people参数的正式类型只是List。调用者的类接口或方法签名中没有任何内容可以通过查看堆栈来告诉您该做什么。

所以你需要实现多个策略,其中一个策略是反编译调用方法的代码,寻找调用者期望一个或另一个类的线索。它必须非常复杂才能涵盖所有可能发生的方式,因为除了其他事项之外,你无法预先知道它实际期望的类,只是期望一个类实现其中一个接口。

有成熟且极其复杂的开源字节码实用程序,包括在运行时自动为给定类生成代理的实用程序(在Java语言支持之前很久就已编写),因此没有&#39 ;用于处理此案例的开源实用程序说明了在实现此方法时努力与有用的比率。

答案 4 :(得分:1)

好的,经过大量的研究,我有另一个想法来完全适应这种情况。由于您无法直接修改其代码......您可以自行强制修改。

免责声明:以下示例代码非常简化。我的目的是展示如何做到这一点的一般方法,而不是生成有效的源代码(因为这本身就是一个项目)。

问题是这些方法是单应性的。所以要解决它,我们可以重命名方法。简单吧?我们可以使用Instrument package来实现这一目标。正如您在链接文档中看到的那样,它允许您创建一个“代理”,可以在加载类时直接修改类,或者即使它们已经加载也可以重新修改它们。

基本上,这需要你创建两个类:

  • 预处理和重新加载类的代理类;和,
  • ClassFileTransformer实施,指定您要进行的更改。

代理类必须定义premain()agentmain()方法,具体取决于您是希望它在JVM启动时还是在它已经运行之后开始处理。这方面的例子在上面的包文档中。通过这些方法,您可以访问Instrumenation实例,这样您就可以注册ClassFileTransformer。所以它可能看起来像这样:

<强> InterfaceFixAgent.java

public class InterfaceFixAgent {

    public static void premain(String agentArgs, Instrumentation inst) {

        //Register an ArtistTransformer
        inst.addTransformer(new ArtistTransformer());

        //In case the Artist interface or its subclasses 
        //have already been loaded by the JVM
        try {
            for(Class<?> clazz : inst.getAllLoadedClasses()) {
                if(Artist.class.isAssignableFrom(clazz)) {
                    inst.retransformClasses(clazz);
                }
            }
        }
        catch(UnmodifiableClassException e) {
            //TODO logging
            e.printStackTrace();
        }
    }
}

<强> ArtistTransformer.java

public class ArtistTransformer implements ClassFileTransformer {

    private static final byte[] BYTES_TO_REPLACE = "complement".getBytes();
    private static final byte[] BYTES_TO_INSERT = "compliment".getBytes();

    @Override
    public byte[] transform(ClassLoader loader, String className,
                            Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) throws IllegalClassFormatException {

        if(Artist.class.isAssignableFrom(classBeingRedefined)) {
            //Loop through the classfileBuffer, find sequences of bytes
            //which match BYTES_TO_REPLACE, replace with BYTES_TO_INSERT
        }
        else return classfileBuffer;
    }

这当然是简化的。它会在extendsimplements Artist的任何类中将“补语”替换为“补语”,因此您很可能需要进一步对其进行条件化(例如,如果Artist.class.isAssignableFrom(classBeingRedefined) && Set.class.isAssignableFrom(classBeingRedefined),则为显然不希望用“赞美”替换“补充”的每个实例,因为Set的“补充”是完全合法的。)

所以,现在我们已经纠正了Artist接口及其实现。错字消失了,方法有两个不同的名字,所以没有单应性。这允许我们现在在CommunityTheatre类中有两个不同的实现,每个实现都将正确地实现/覆盖ArtistSet中的方法。

不幸的是,我们现在已经创建了另一个(可能更大)的问题。我们刚刚从实现complement()的类中删除了对Artist的所有以前合法的引用。要解决此问题,我们需要创建另一个ClassFileTransformer,用我们的新方法名称替换这些调用。

这有点困难,但并非不可能。基本上,新ClassFileTransformer(假设我们称之为OldComplementTransformer)必须执行以下步骤:

  1. 找到与之前相同的字节字符串(表示旧方法名称的字符串,“补充”);
  2. 获取之前的字节,表示调用方法的对象引用;
  3. 将这些字节转换为Object;
  4. 检查Object是否为Artist;和,
  5. 如果是,请使用新方法名称替换这些字节。
  6. 一旦你制作了第二个变换器,你可以修改InterfaceFixAgent以适应它。 (我还简化了retransformClasses()调用,因为在上面的示例中,我们在变换器本身内执行所需的检查。)

    InterfaceFixAgent.java 已修改

    public class InterfaceFixAgent {
    
        public static void premain(String agentArgs, Instrumentation inst) {
    
            //Register our transformers
            inst.addTransformer(new ArtistTransformer());
            inst.addTransformer(new OldComplementTransformer());
    
            //Retransform the classes that have already been loaded
            try {
                inst.retransformClasses(inst.getAllLoadedClasses());
            }
            catch(UnmodifiableClassException e) {
                //TODO logging
                e.printStackTrace();
            }
        }
    }
    

    现在......我们的计划很好。编码肯定不容易,而且QA和测试将是彻头彻尾的地狱。但它确实很强大,它解决了这个问题。 (从技术上讲,我认为通过删除它来避免问题,但是......我将采取我能得到的东西。)

    我们可能解决问题的其他方式:

    这两个都允许你直接操作内存中的字节。当然可以围绕这些解决方案来设计解决方案,但我相信它会更加困难并且更不安全。所以我选择了上面的路线。

    我认为这个解决方案甚至可以更加通用,成为一个非常有用的库,用于集成代码库。指定在变量,命令行参数或配置文件中需要重构的接口和方法,让她松散。在运行时协调Java中冲突接口的库。 (当然,我认为如果他们只修复了Java 8中的错误,对每个人来说仍然会更好。)

答案 5 :(得分:0)

以下是我要采取的措施,以消除歧义:

interface Artist {
    void complement(); // [SIC] from OP, really "compliment"
    int getEgo();
}

interface Set {
    void complement(); // as in Set Theory
    void remove();
    boolean empty(); // [SIC] from OP, I prefer: isEmpty()
}

/**
 * This class is to represent a Set of Artists (as a group) -OR-
 * act like a single Artist (with some aggregate behavior).  I
 * choose to implement NEITHER interface so that a caller is
 * forced to designate, for any given operation, which type's
 * behavior is desired.
 */
class GroupOfArtists { // does NOT implement either

    private final Set setBehavior = new Set() {
        @Override public void remove() { /*...*/ }
        @Override public boolean empty() { return true; /* TODO */ }            
        @Override public void complement() {
            // implement Set-specific behavior
        }
    };

    private final Artist artistBehavior = new Artist() {
        @Override public int getEgo() { return Integer.MAX_VALUE; /* TODO */ }            
        @Override public void complement() {
            // implement Artist-specific behavior
        }
    };

    Set asSet() {
        return setBehavior;
    }

    Artist asArtist() {
        return artistBehavior;
    }
}

如果我将此对象传递给人力资源部门,我实际上会给它从asSet()返回的值来雇用/解雇整个团队。

如果我将此物品传递给剧院进行表演,我实际上会将asArtist()返回的值视为天赋。

只要您能够直接与不同的组件交谈,这就有效...

但我意识到您的问题是单个第三方供应商创建了一个组件TheaterManager,该组件期望这两个函数都有一个对象,它不会知道asSetasArtist方法。问题不在于创建SetArtist的供应商,而是供应商将它们组合在一起而不是使用访客模式,或仅指定一个将镜像asSet和{的接口{1}}我上面提到的方法。如果你可以说服你的一个供应商“C”来修复那个界面,你的世界将会更加快乐。

祝你好运!

答案 6 :(得分:-1)

狗,我有一种强烈的感觉,你遗漏了一些对解决方案至关重要的细节。这通常发生在SO上,因为

  • 人们需要遗漏很多细节才能将问题调到合理的范围和范围,
  • 人们并不完全理解问题和解决方案(这就是他们寻求帮助的原因)所以他们无法确定哪些细节很重要,哪些不重要,
  • 这个人不能自己解决问题的原因是因为他们不了解这个细节的重要性,这就是他们遗漏的原因。

我在另一个回答中说过我会对ArtistSet做些什么。但是记住上面的内容我会给你一个稍微不同的问题的另一个解决方案。假设我有来自坏供应商的代码:

package com.bad;

public interface IAlpha {
    public String getName();
    // Sort Alphabetically by Name
    public int compareTo(IAlpha other);
}

这很糟糕,因为你应该声明一个返回Comparator<IAlpha>的函数来实现排序策略,但无论如何。现在我从更糟糕的公司获得代码:

package com.worse;
import com.bad.IAlpha;

// an Alpha ordered by name length
public interface ISybil extends IAlpha, Comparable<IAlpha> {}

这更糟糕,因为它完全错误,因为它会不相容地覆盖行为。 ISybil按名称长度排序,但IAlpha按字母顺序排序,但ISybil IAlpha除外。当他们可以而且应该做的事情时,他们被IAlpha的反模式误导了:

public interface ISybil extends IAlpha {
  public Comparator<IAlpha> getLengthComparator();
}

然而,这种情况仍然比ArtistSet好得多,因为这里记录了预期的行为。关于ISybil.compareTo()应该做什么没有混淆。所以我会按如下方式创建类。实现compareTo()作为com.worse的Sybil类需要并委托其他所有内容:

package com.hack;

import com.bad.IAlpha;
import com.worse.ISybil;

public class Sybil implements ISybil {

    private final Alpha delegate;

    public Sybil(Alpha delegate) { this.delegate = delegate; }
    public Alpha getAlpha() {   return delegate; }
    public String getName() { return delegate.getName(); }
    public int compareTo(IAlpha other) {
        return delegate.getName().length() - other.getName().length();
    }

}

和一个完全像com.bad一样工作的Alpha类应该:

package com.hack;
import com.bad.IAlpha;

public class Alpha implements IAlpha {
    private String name;
    private final Sybil sybil;
    public Alpha(String name) { 
        this.name = name;
        this.sybil = new Sybil(this);
    }

    // Sort Alphabetically
    public int compareTo(IAlpha other) {
        return name.compareTo(other.getName());
    }

    public String getName() { return name; }
    public Sybil getSybil() { return sybil; }
}

请注意,我包含了类型转换方法:Alpha.getSybil()和Sybil.getAlpha()。这样我就可以围绕任何com.worse供应商的方法创建自己的包装器,这些方法可以使用或返回Sybils,因此我可以避免使用com.worse的破坏来污染我的代码或任何其他供应商的代码。所以如果com.worse有:

public ISybil breakage(ISybil broken);

我可以写一个函数

public Alpha safeDelegateBreakage(Alpha alpha) {
  return breakage(alpha.getSybil).getAlpha();
}

并完成它,除了我仍然会大声抱怨com.worse和礼貌地com.bad。