Java中的#define等价物是什么?

9
我正在编写一个库,需要在特定库被包含时添加一些代码。由于这些代码分散在整个项目中,如果用户不必自己注释/取消注释将很好。
在C语言中,可以在头文件中使用#define,然后用#ifdefs包围代码块来实现。当然,Java没有C预处理器...
为了澄清 - 我的库将与多个外部库一起分发。我不想将它们全部包含以最小化可执行文件大小。如果开发人员确实包含了一个库,我需要能够使用它,如果没有,它就可以被忽略掉。
在Java中怎样做最好?

请查看此答案,https://dev59.com/cXI-5IYBdhLWcg3wSGUB#48330157 - TT--
12个回答

11

无法在Java内部完成您想要的操作。您可以预处理Java源文件,但这超出了Java的范围。

您不能抽象化差异然后变化实现吗?

根据您的澄清,听起来您可能能够创建一个工厂方法,该方法将返回来自外部库之一的对象,或者是一个“桩”类,其函数将执行您在“不可用”条件代码中应执行的操作。


+1:这个问题听起来可能可以修改解决方案架构设计来帮助解决。 - Russell
Java编译器将优化基于常量值的条件检查。因此这是完全可能的。 - Anon.
@Anon: 但它仍然编译了两个分支。它只是优化掉了未使用的代码,对吧?此外,很多时候,为了消除这些问题,重新构建架构可能会变得非常混乱。如果你有一堆想要 #ifdefed 的随机位置... 你最终会得到一个基类,然后是一堆子类,带有辅助函数,这只会变成一团糟。 - Brian Postow
你有一个类,没有辅助函数,还有一堆符号常量。我看不出需要对任何东西进行子类化。 - Anon.
为了确定是否存在设计解决方案,我们需要更多细节。 - Steve Emmerson
@Anon,对不起,我把你和原始回答者(Steve)以及Russell搞混了。优化并没有解决“无法编译”的问题。子类化和辅助程序是对架构/抽象解决方案的响应。 - Brian Postow

6
正如其他人所说,Java中没有#define/#ifdef这样的东西。但是关于您拥有可选外部库的问题,如果存在,则会使用它们,否则不使用,使用代理类可能是一种选择(如果库接口不太大)。
我曾经为AWT/Swing的Mac OS X特定扩展(在com.apple.eawt.*中找到)做过这个工作。当然,如果应用程序在Mac OS上运行,这些类只在类路径上。为了能够使用它们,但仍允许在其他平台上使用相同的应用程序,我编写了简单的代理类,它们只提供与原始EAWT类相同的方法。在内部,代理使用一些反射来确定真实类是否在类路径上,并将通过所有方法调用。通过使用java.lang.reflect.Proxy类,甚至可以创建并传递在编译时未定义的外部库中定义的类型的对象。
例如,com.apple.eawt.ApplicationListener的代理如下:
public class ApplicationListener {

    private static Class<?> nativeClass;

    static Class<?> getNativeClass() {
        try {
            if (ApplicationListener.nativeClass == null) {
                ApplicationListener.nativeClass = Class.forName("com.apple.eawt.ApplicationListener");
            }

            return ApplicationListener.nativeClass;
        } catch (ClassNotFoundException ex) {
            throw new RuntimeException("This system does not support the Apple EAWT!", ex);
        }
    }

    private Object nativeObject;

    public ApplicationListener() {
        Class<?> nativeClass = ApplicationListener.getNativeClass();

        this.nativeObject = Proxy.newProxyInstance(nativeClass.getClassLoader(), new Class<?>[] {
            nativeClass
        }, new InvocationHandler() {

            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                String methodName = method.getName();

                ApplicationEvent event = new ApplicationEvent(args[0]);

                if (methodName.equals("handleReOpenApplication")) {
                    ApplicationListener.this.handleReOpenApplication(event);
                } else if (methodName.equals("handleQuit")) {
                    ApplicationListener.this.handleQuit(event);
                } else if (methodName.equals("handlePrintFile")) {
                    ApplicationListener.this.handlePrintFile(event);
                } else if (methodName.equals("handlePreferences")) {
                    ApplicationListener.this.handlePreferences(event);
                } else if (methodName.equals("handleOpenFile")) {
                    ApplicationListener.this.handleOpenFile(event);
                } else if (methodName.equals("handleOpenApplication")) {
                    ApplicationListener.this.handleOpenApplication(event);
                } else if (methodName.equals("handleAbout")) {
                    ApplicationListener.this.handleAbout(event);
                }

                return null;
            }

        });
    }

    Object getNativeObject() {
        return this.nativeObject;
    }

    // followed by abstract definitions of all handle...(ApplicationEvent) methods

}

所有这些只有在您需要从外部库中获取少量类时才有意义,因为您必须在运行时通过反射来完成所有操作。对于较大的库,您可能需要某种自动化代理生成方式。但是,如果您真的那么依赖于一个大型的外部库,那么您应该在编译时要求它。
Peter Lawrey的评论:(抱歉要编辑,将代码放入注释很难)
以下示例是按方法通用的,因此您不需要知道涉及的所有方法。您还可以按类通用,以便只需编写一个InvocationHandler类即可覆盖所有情况。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String methodName = method.getName();
    ApplicationEvent event = new ApplicationEvent(args[0]);
    Method method = ApplicationListener.class.getMethod(methodName, ApplicationEvent.class);
    return method.invoke(ApplicationListener.this, event);
}

5

+1 依赖注入。想象一下,通过 XML 文件(例如)连接您的对象,然后使用不同的 XML 文件来处理各种情况。 - Michael Easter

4

嗯,Java语法非常接近C语言,你可以简单地使用通常作为单独可执行文件提供的C预处理器。

但是Java真正的优势不在于编译时执行代码。我之前处理类似情况的方式是通过反射。在你的情况下,由于对可能不存在的库的调用散布在代码中,我会创建一个包装类,将所有对该库的调用替换为对包装类的调用,然后在包装类内部使用反射来调用该库(如果存在)。


2

使用常量

本周我们创建了一些常量,具有使用C预处理器的功能来定义编译时常量和条件编译代码的所有优点。

如果将Java视为C/C++的“后代”,则Java已经摆脱了文本预处理器的整个概念。然而,在Java中,我们可以获得至少一些C预处理器功能的最佳效果:常量和条件编译。


我认为这不是他想要的。他不想要符号常量,而是想要条件编译的等效物。我不了解Java,所以我一点头绪也没有... - dmckee --- ex-moderator kitten
条件编译怎么样?在Java中如何实现? - Russell
我链接的文章讨论了这个问题。 - Andrew Hare
1
条件编译仍然只在所有内容都编译成功的情况下才能工作。如果then分支为true时,else分支无法编译,因为这些方法不存在或其他原因,则它将无法编译。#if可以很好地解决这个问题。 - Brian Postow
是的,您真的不希望系统中存在无法编译的代码。使用版本控制以针对构建,或者可以使用方法存根。最坏的情况是,如果您确实必须拥有链接到在编译时不可用的库的代码,则可以使用反射。 - Bill K
显示剩余2条评论

1

我有一个更好的表述方式。

你需要的是一个常量变量。

public static final boolean LibraryIncluded= false; //or true - manually set this

然后在代码内部这样写:
if(LibraryIncluded){
    //do what you want to do if library is included
}
else
{
    //do if you want anything to do if the library is not included
}

这将起到 #ifdef 的作用。任何一个代码块都将出现在可执行代码中。其他的将在编译时被消除。

1

我不相信这种事情真的存在。大多数真正的Java用户会告诉你,这是一件好事,并且应该尽可能避免依赖条件编译。

我并不完全同意他们的看法...

你可以使用可以从编译行中定义的常量,这将产生一些效果,但并非全部。(例如,您不能在#if 0内部拥有不编译但仍然需要的内容...(而且,注释并不总是解决这个问题,因为嵌套注释可能很棘手...))。

我认为大多数人会告诉你使用某种形式的继承来完成这个任务,但这也可能非常丑陋,代码重复很多...

话虽如此,你总是可以设置你的IDE,在将Java发送到javac之前通过预处理器进行处理...


1

"为了减小我可执行文件的大小"

你所说的“可执行文件大小”是什么意思?

如果你指的是运行时加载的代码量,那么你可以通过类加载器有条件地加载类。这样,无论如何你都会分发你的替代代码,但只有在缺少该库时才会实际加载它。你可以使用适配器(或类似物)来封装API,以确保无论哪种方式,几乎所有的代码都是完全相同的,根据情况加载其中的两个包装类之一。Java安全SPI可能会给你一些关于如何构建和实现这种结构的想法。

如果你指的是你的.jar文件的大小,那么你可以执行上述操作,但要告诉你的开发人员如何从jar文件中剥离不必要的类,当他们知道这些类不会被使用时。


该库将用于Android应用程序,因此我不想包含任何不必要的内容。 - Justin
这不符合您原始问题的参数。如果您在运行时进行链接,仍然必须编译调用库的代码,因此#ifdef也无济于事。如果您没有在运行时链接但是为不同的目标编译,则if(CONSTANT)就足够了。哪一个才是实际的问题? - Bill K
不,常量是不够的。例如,if(false) {LibraryIDontHave.action();} 将无法编译。 - Justin

0

根据你正在做什么(信息不够充分),你可以像这样做:

interface Foo
{
    void foo();
}

class FakeFoo
    implements Foo
{
   public void foo()
   {
       // do nothing
   }
}

class RealFoo
{
    public void foo()
    {
        // do something
    }
}

然后提供一个抽象实例化的类:

class FooFactory
{
    public static Foo makeFoo()
    {
        final String   name;
        final FooClass fooClass;
        final Foo      foo;

        name     = System.getProperty("foo.class");
        fooClass = Class.forName(name);
        foo      = (Foo)fooClass.newInstance();

        return (foo);
    }
}

然后使用 -Dfoo.name=RealFoo|FakeFoo 运行Java。

忽略makeFoo方法中的异常处理,你也可以用其他方式实现...但是思路是一样的。

这样,你就可以编译Foo子类的两个版本,并让开发人员在运行时选择他们想要使用的版本。


0

我看到你在这里指定了两个互斥的问题(或者更可能的是,你已经选择了一个,而我只是没有理解你做出了哪个选择)。

你必须做出一个选择:你是要发布两个版本的源代码(一个是如果库存在的话,另一个是如果不存在的话),还是要发布一个单一版本,并期望它在库存在时能够正常工作。

如果你想要一个单一版本来检测库的存在并在可用时使用它,则必须在你分发的代码中拥有所有访问它的代码--你不能将其删除。由于你将你的问题与使用 #define 相提并论,我认为这不是你的目标--你想要发布 2 个版本(#define 可以工作的唯一方式)

因此,有了 2 个版本,你可以定义一个 libraryInterface。这可以是一个对象,它包装你的库并为你转发所有调用,也可以是一个接口--无论哪种情况,这个对象都必须在编译时存在于两种模式中。

public LibraryInterface getLibrary()
{
    if(LIBRARY_EXISTS) // final boolean
    {
        // Instantiate your wrapper class or reflectively create an instance             
        return library; 
    }
    return null;
}

现在,当您想要使用您的库(在C中可能需要使用#ifdef的情况)时,您可以这样做:
if(LIBRARY_EXISTS)
    library.doFunc()

Library(库)是一个存在于两种情况下的接口。由于它总是受LIBRARY_EXISTS保护,它将被编译掉(甚至不会加载到您的类加载器中-但这取决于实现)。

如果您的库是由第三方提供的预打包库,则可能必须使Library成为一个包装器类,将其调用转发给您的库。由于如果LIBRARY_EXISTS为false,则永远不会实例化您的库包装器,因此在运行时甚至不应加载它(嘿,如果JVM足够聪明,它甚至不应该被编译,因为它总是受最终常量保护)。但请记住,在两种情况下,包装器在编译时必须可用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接