飙血推荐
  • HTML教程
  • MySQL教程
  • JavaScript基础教程
  • php入门教程
  • JavaScript正则表达式运用
  • Excel函数教程
  • UEditor使用文档
  • AngularJS教程
  • ThinkPHP5.0教程

Java 中的运算符重载

时间:2023-06-06  作者:电脑狂魔  

在本文中,我们将深入探讨 Java 中运算符重载的迷人世界。尽管 Java 本身不支持运算符重载,但我们将发现 Manifold 如何使用该功能扩展 Java。我们将探索它的好处、局限性和用例,尤其是在科学和数学代码中。

我们还将探索 Manifold 提供的三个强大功能,这些功能增强了默认的 Java 类型安全性,同时支持令人印象深刻的编程技术。我们将讨论单元表达式、类型安全的反射编码和在编译期间修复方法,如 equals。此外,我们将介绍 Manifold 提供的解决关键字某些限制的解决方案var。让我们开始吧!

在我们开始之前,您可以一如既往地在我的GitHub 页面上找到本博文和本系列其他视频的代码示例。请务必查看该项目,给它一个星标,并在 GitHub 上关注我以保持更新!

算术运算符

运算符重载允许我们在代码中使用熟悉的数学符号,使其更具表现力和直观性。虽然 Java 默认不支持运算符重载,但 Manifold 提供了解决此限制的方法。

为了演示,让我们从一个Vector执行向量算术运算的简单类开始。在标准 Java 代码中,我们定义变量,在构造函数中接受它们,并实现plus向量加法等方法。但是,这种方法可能比较冗长且可读性较差。

public class Vec {
   private float x, y, z;
   public Vec(float x, float y, float z) {
       this.x = x;
       this.y = y;
       this.z = z;
   }
   public Vec plus(Vec other) {
       return new Vec(x + other.x, y + other.y, z + other.z);
   }
}

使用 Manifold,我们可以显着简化代码。使用 Manifold 的运算符重载功能,我们可以直接使用运算符将​​向量相加+:

Vec vec1 = new Vec(1, 2, 3);
Vec vec2 = new Vec(1, 1, 1);
Vec vec3 = vec1 + vec2;

Manifold 将运算符无缝映射到适当的方法调用,使代码更简洁。这种流畅的语法类似于数学符号,增强了代码的可读性。

此外,Manifold 可以优雅地处理反向符号。假设我们把操作数的顺序颠倒过来,比如一个标量加一个向量,Manifold 调换顺序并正确执行运算。这种灵活性使我们能够以更自然和直观的方式编写代码。

假设我们将其添加到 Vec 类:

public Vec plus(float other) {
    return new Vec(x + other, y + other, z + other);
}

这将使所有这些行有效:

vec3 += 域名;
vec3 = 域名 + vec3;
vec3 = vec3 + 域名;
vec3 += 域名eOf(域名);

在此代码中,我们演示了 Manifold 可以交换顺序以域名(float)无缝调用。我们还表明加号等于运算符支持内置于加号方法支持中

正如前面的代码所暗示的那样,Manifold 还支持原始包装器对象,特别是在自动装箱的上下文中。在 Java 中,基本类型有相应的包装对象。由于自动装箱和拆箱,Manifold 无缝地处理基元与其包装对象之间的转换。这使我们能够在代码中交替使用对象和原语。正如我们将发现的那样,对此有一些警告。

BigDecimal 支持

Manifold 超越了简单的算术运算,支持更复杂的场景。例如,manifold-science依赖项包括对算术的内置支持BigDecimal。BigDecimal是用于涉及大数或金融计算的精确计算的Java类吗?通过使用 Manifold,我们可以使用熟悉的运算符(例如+、-、*和 )对 BigDecimal 对象执行算术运算/。Manifold 的集成BigDecimal简化了代码并确保了准确的计算。

一旦我们添加了正确的依赖集,即向类添加方法扩展,下面的代码就是合法的BigDecimal:

var x = new BigDecimal(5L);
var y = new BigDecimal(25L);
var z = x + y;

在幕后,Manifold 将适用的 plus、minus、times 等方法添加到类中。它通过利用我之前讨论过的类扩展来做到这一点。

拳击极限

我们还可以扩展现有类以支持运算符重载。Manifold 允许我们扩展类并添加接受自定义类型或执行特定操作的方法。例如,我们可以扩展该类Integer并添加一个plus接受 BigDecimal 作为参数并返回BigDecimal结果的方法。此扩展使我们能够无缝地执行不同类型之间的算术运算。目标是让这段代码编译:

var z = 5 + x + y;

不幸的是,这不会与该更改一起编译。数字 5 是原始值,而不是整数,让该代码正常工作的唯一方法是:

var z = 域名eOf(5) + x + y;

这不是我们想要的。但是,有一个简单的解决方案。我们可以为自己创建一个扩展BigDecimal,并依赖于顺序可以无缝交换的事实。这意味着这个简单的扩展可以支持5 + x + y表达式而无需更改:

@Extension
public class BigDecimalExt {
    public static BigDecimal plus(@This BigDecimal b, int i) {
        return 域名(域名eOf(i));
    }
}

算术运算符列表

到目前为止,我们关注的是加号运算符,但 Manifold 支持范围广泛的运算符。下表列出了方法名称及其支持的运算符:

操作员方法
+,+=plus
-,-=minus
*,*=times
/,/=div
%,%=rem
-aunaryMinus
++inc
--dec

请注意,递增和递减运算符在前缀和后缀定位之间没有区别。两者a++都会++a导致该inc方法。

索引运算符

对索引运算符的支持让我在看的时候完全措手不及。这完全改变了游戏规则……索引运算符是我们用来通过索引获取数组值的方括号。为了让您了解我在说什么,这是 Manifold 中的有效代码:

var list = 域名("A", "B", "C");
var v = list[0];

在这种情况下,vwill be“A”和代码等同于调用域名(0). 索引运算符无缝映射到获取和设置方法。我们也可以使用以下方法进行分配:

var list = new ArrayList<>(域名("A", "B", "C"));
var v = list[0];
list[0] = "1";

请注意,我必须将 List 包装在 an 中,ArrayList因为域名()它返回了一个不可修改的 List。但这不是我要纠结的部分。该代码“不错”。这段代码绝对令人惊叹:

var map = new HashMap<>(域名("Key", "Value"));
var key = map["Key"];
map["Key"] = "New Value";

是的!

您正在阅读 Manifold 中的有效代码。索引运算符用于在地图中查找。请注意,映射具有put() 方法而不是set方法。这是 Manifold 使用扩展方法修复的一个恼人的不一致问题。然后我们可以使用一个对象来使用运算符在地图中查找。

关系运算符和相等运算符

我们还有很多东西要讲……我们可以这样写代码吗(指的是Vec之前的对象):

if(vec3 > vec2) {
    // …
}

默认情况下不会编译。但是,如果我们将Comparable接口添加到类中,Vec这将按预期工作:

public class Vec implements Comparable<Vec> {
    // …
    public double magnitude() {
        return 域名(x  x + y  y + z * z);
    }
    @Override
    public int compareTo(Vec o) {
        return 域名are(magnitude(), 域名itude());
    }
}

通过调用该方法,这些>=, >, <, <=比较运算符将完全按照预期工作compareTo。但是有一个大问题。您会注意到此列表中缺少==and运算符。!=在Java中,我们经常使用这些运算符来进行指针比较。这在性能方面很有意义。我们不想改变 Java 中如此固有的东西。为避免这种情况,Manifold 默认情况下不会覆盖这些运算符。

但是,我们可以实现ComparableUsing接口,它是接口的子接口Comparable。一旦我们这样做了,==and!=将默认使用 equals 方法。equalityMode()我们可以通过覆盖可以返回这些值之一的方法来覆盖该行为:

  • CompareTo==— 将对和使用 compareTo 方法!=

  • Equals(默认)——将使用 equals 方法

  • Identity— 将使用指针比较作为 Java 中的规范

该接口还允许我们覆盖该compareToUsing(T, Operator)方法。这类似于 compareTo 方法,但允许我们创建特定于运算符的行为,这在某些边缘情况下可能很重要。

科学编码的单位表达式

请注意,Unit 表达式在 Manifold 中是实验性的。但它们是运算符重载在这种情况下最有趣的应用之一。

单位表达式是一种新型的运算符,它在强制强类型化的同时显着简化和增强了科学编码。使用单位表达式,我们可以为包含单位类型的数学表达式定义符号。这为科学计算带来了新的清晰度和安全类型。

例如,考虑一个距离计算,其中速度定义为每小时 100 英里。通过将速度(英里/小时)乘以时间(小时),我们可以获得这样的距离:

Length distance = 100 mph * 3 hr;
Force force = 5kg * 域名 m/s/s;
if(force == 域名 N) {
    // true
}

单位表达式允许我们表达数值(或变量)及其相关单位。编译器检查单位的兼容性,防止不兼容的转换并确保准确计算。此功能简化了科学代码并轻松实现强大的计算。

在幕后,单元表达式只是一个转换调用。表达式100 mph转换为:

域名fixBind(域名eOf(100))

该表达式返回一个 Velocity 对象。该表达式3 hr类似地绑定到后缀方法并返回一个 Time 对象。此时,ManifoldVelocity类有一个times方法,正如您所记得的那样,它是一个运算符,它会在两个结果上调用:

public Length times( Time t ) {
    return new Length( toBaseNumber() * 域名seNumber(), 域名, getDisplayUnit().getLengthUnit() );
}

请注意,该类具有多个接受不同对象类型的 times 方法的重载版本。一个Velocity时代Mass就会产生Momentum。一次结果。Velocity_ForcePower

即使在这个早期的实验阶段,许多单位都作为这个包的一部分得到支持,请在此处查看。

您可能会注意到这里有一个很大的遗漏:货币。我很想有这样的东西:

var sum = 50 USD + 70 EUR;

如果您查看该代码,问题应该很明显。我们需要汇率。如果没有汇率和可能的转换成本,这是没有意义的。财务计算的复杂性并不能很好地转化为代码的当前状态。我怀疑这就是这仍然是实验性的原因。我很好奇如何优雅地解决这样的问题。

运算符重载的陷阱

虽然 Manifold 提供了强大的运算符重载功能,但重要的是要注意潜在的挑战和性能考虑因素。Manifold 的方法会导致额外的方法调用和对象分配,这可能会影响性能,尤其是在性能关键的环境中。考虑优化技术至关重要,例如减少不必要的方法调用和对象分配,以确保高效的代码执行。

让我们看看这段代码:

var n = x + y + z;

从表面上看,它似乎高效而简短。它在物理上转化为这段代码:

var n = 域名(y).plus(z);

这仍然很难发现,但请注意,为了创建结果,我们调用了两个方法并分配了至少两个对象。一种更有效的方法是:

var n = 域名(y, z);

这是我们针对高性能矩阵计算经常做的优化。如果性能很重要,您需要注意这一点并了解操作员在幕后做了什么。我不想暗示运算符天生就比较慢。事实上,它们与方法调用一样快,但有时具体调用的方法和分配的数量并不直观。

类型安全特性

以下内容与运算符重载无关,但它们是第二个视频的一部分,因此我认为它们作为类型安全广泛讨论的一部分是有意义的。我最喜欢 Manifold 的地方之一是它支持严格的类型和编译时错误。对我来说,两者都代表了 Java 的核心精神。

越狱:类型安全的反射

@JailBreak是一种授予访问类内私有状态的功能。虽然这听起来很糟糕,但@JailBreak它提供了一种比使用传统反射访问私有变量更好的替代方法。通过越狱一个类,我们可以无缝地访问它的私有状态,而编译器仍然在执行类型检查。从这个意义上说,这是两害相权取其轻。如果你打算做一些糟糕的事情(访问私有状态),那么至少让编译器检查一下。

在下面的代码中,值数组是 String 私有的,但由于注解,我们可以对其进行操作@JailBreak。此代码将打印“Ex0osed…”:

@Jailbreak String exposedString = "Exposed...";
域名e[2] = '0';
域名tln(exposedString);

JailBreak 也可以应用于静态字段和方法。但是,访问静态成员需要将 null 分配给变量,这似乎违反直觉。尽管如此,此功能提供了一种更受控制且类型安全的方法来访问内部状态,从而最大限度地减少了与使用反射相关的风险。

@Jailbreak String str = null;
域名CII(new byte[] { 111, (byte)222 });

最后,Manifold 中的所有对象都注入了一个jailbreak()方法。这个方法可以像这样使用(注意这fastTime是一个私有字段):

Date d = new Date();
long t = 域名break().fastTime;

自注释:强制方法参数类型

在 Java 中,某些 API 接受对象作为参数,即使可以使用更具体的类型也是如此。这可能会在运行时导致潜在的问题和错误。但是,Manifold 引入了@Self注释,这有助于强制执行作为参数传递的对象的类型。

通过用 注释参数@Self,我们明确声明只接受指定的对象类型。这确保了类型安全并防止意外使用不兼容的类型。使用此注释,编译器可以在开发过程中捕获此类错误,从而降低在生产中遇到问题的可能性。

让我们看看MySizeClass我以前的帖子:

public class MySizeClass {
    int size = 5;
    public int size() {
        return size;
    }
    public void setSize(int size) {
        域名 = size;
    }
    public boolean equals(@Self Object o) {
        return o != null && ((MySizeClass)o).size == size;
    }
}

请注意,我添加了一个 equals 方法并用 Self 注释了参数。如果我删除 Self 注释,此代码将编译:

var size = new MySizeClass();
域名ls("");
域名ls(new MySizeClass());

有了@Self注解,编译时字符串比较就会失败。

Auto 关键字:Var 的更强大替代品

我不是var关键字的忠实粉丝。我觉得它并没有简化多少,代价是编码到一个实现而不是一个接口。我理解为什么 Oracle 的开发人员选择了这条路。保守的决定是我发现 Java 如此有吸引力的主要原因。Manifold 的好处是可以在这些限制之外工作,并且它提供了一个更强大的替代方案,称为auto. auto可以在字段和方法返回值中使用,使其比 var 更灵活。它提供了一种在不牺牲类型安全性的情况下定义变量的简洁而富有表现力的方式。

Auto 在处理元组时特别有用,这是本文尚未讨论的功能。它允许优雅和简洁的代码,增强可读性和可维护性。您可以有效地使用 auto 作为 var 的直接替代品。

最后

使用 Manifold 的运算符重载为 Java 带来了富有表现力和直观的数学符号,增强了代码的可读性和简单性。虽然 Java 本身不支持运算符重载,但 Manifold 使开发人员能够实现类似的功能并在他们的代码中使用熟悉的运算符。通过利用 Manifold,我们可以编写更流畅、更具表现力的代码,尤其是在科学、数学和金融应用程序中。

Manifold 中的安全增强类型使 Java 更……好吧,“类 Java”。它使 Java 开发人员可以在该语言的强大基础上构建并采用更具表现力的类型安全编程范例。

我们应该给 Java 本身添加运算符重载吗?

我不赞成。我喜欢 Java 缓慢、稳定和保守。我也喜欢 Manifold 的大胆和冒险精神。这样,当我在做一个项目时,我可以选择它,这种方法是有意义的(例如,一个启动项目),但为企业项目选择标准的保守 Java。


湘ICP备14001474号-3  投诉建议:234161800@qq.com   部分内容来源于网络,如有侵权,请联系删除。