在这篇文章中,我们将深入研究Java的运营商超载世界。尽管Java并没有本地支持操作员的过载,但我们将发现多种多样的Java如何通过该功能扩展Java。我们将探讨其优势,局限性和用例,特别是在科学和数学代码中。
我们还将探索歧管提供的三个强大功能,这些功能增强了默认的Java类型安全性,同时实现了令人印象深刻的编程技术。我们将讨论单位表达式,类型安全的反射编码以及汇编过程中的固定方法。此外,我们将介绍一种解决方案,该解决方案提供了解决var
关键字的某些局限性的解决方案。让我们潜入!
在我们一如既往的开始之前,您可以在我的GitHub page上找到本帖子的代码示例以及本系列中的其他视频。一定要检查项目,给它星星,然后在Github上关注我以保持更新!
算术操作员
操作员的过载使我们能够在代码中使用熟悉的数学符号,从而使其更具表现力和直观。虽然Java默认不支持操作员过载,但歧管为此限制提供了解决方案。
要演示,让我们从执行向量算术操作的简单Vector
类开始。在标准Java代码中,我们定义变量,在构造函数中接受它们,并实现诸如plus
for Vector添加之类的方法。但是,这种方法可以是冗长的,而且可读性较低。
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);
}
}
使用歧管,我们可以显着简化代码。使用歧管的运算符过载功能,我们可以使用+
操作员直接将向量添加在一起:
Vec vec1 = new Vec(1, 2, 3);
Vec vec2 = new Vec(1, 1, 1);
Vec vec3 = vec1 + vec2;
将操作员无缝地映射到适当的方法调用,使代码清洁器和更简洁。该流体语法类似于数学符号,增强了代码可读性。
此外,多种多样处理反向符号。如果我们逆转操作数的顺序,例如标量和向量,则可以放风订单并正确执行操作。这种灵活性使我们能够以更自然和直观的方式编写代码。
我们说我们将其添加到VEC类:
public Vec plus(float other) {
return new Vec(x + other, y + other, z + other);
}
这将使所有这些行有效:
vec3 += 5.0f;
vec3 = 5.0f + vec3;
vec3 = vec3 + 5.0f;
vec3 += Float.valueOf(5.0f);
在此代码中,我们证明了歧管可以将命令交换以无缝调用Vec.plus(float)
。我们还表明,加号运算符支持位于加号支持
如上一个代码歧管所暗示的还支持在自动氧化的上下文中专门支持原始包装器对象。在Java中,原始类型具有相应的包装对象。歧视通过自动氧化和拆箱,可以无缝地处理原始物与包装器对象之间的转换。这使我们能够在代码中互换与对象和原语一起工作。我们会发现这有警告。
BigDecimal支持
歧视超越了简单的算术,并支持更复杂的场景。例如,manifold-science
依赖项包括对BigDecimal
算术的内置支持。 BigDecimal
是一种Java类,用于涉及大量或财务计算的精确计算。通过使用歧管,我们可以使用熟悉的操作员(例如+
,-
,*
和/
)使用BigDecimal对象执行算术操作。歧管与BigDecimal
的集成简化了代码并确保准确的计算。
一旦我们添加正确的依赖项,以下代码是合法的,将方法扩展添加到BigDecimal
类:
var x = new BigDecimal(5L);
var y = new BigDecimal(25L);
var z = x + y;
在引擎盖下,歧管在类中添加了适用的Plus,减,时间等。它通过利用I discussed before的类扩展来做到这一点。
拳击的极限
我们还可以扩展现有类以支持操作员的过载。歧管允许我们扩展类并添加接受自定义类型或执行特定操作的方法。例如,我们可以扩展Integer
类,并添加一个接受BigDecimal作为参数并返回BigDecimal
结果的plus
方法。此扩展使我们能够在不同类型之间无缝执行算术操作。目的是使该代码编译:
var z = 5 + x + y;
不幸的是,这将随着这种变化而编译。第五是原始的,不是整数,使该代码工作的唯一方法是:
var z = Integer.valueOf(5) + x + y;
这不是我们想要的。但是,有一个简单的解决方案。我们可以为BigDecimal
本身创建一个扩展名,并依靠可以无缝交换订单的事实。这意味着这个简单的扩展可以在没有更改的情况下支持5 + x + y
表达式:
@Extension
public class BigDecimalExt {
public static BigDecimal plus(@This BigDecimal b, int i) {
return b.plus(BigDecimal.valueOf(i));
}
}
算术运算符
到目前为止,我们专注于Plus操作员,但流动支持广泛的运营商。下表列出了方法名称及其支持的操作员:
操作员 | 方法 |
---|---|
+ ,+=
|
plus |
- ,-=
|
minus |
* ,*=
|
times |
/ ,/=
|
div |
% ,%=
|
rem |
-a |
unaryMinus |
++ |
inc |
-- |
dec |
请注意,增量和减少操作员在前缀和后缀定位之间没有区别。 a++
和++a
都将导致inc
方法。
索引操作员
当我看时,对索引操作员的支持使我完全措手不及。这是一个完整的游戏改变者…索引运算符是我们用来通过索引获取数组值的方括号。为了让您了解我在说什么,这是多方面的有效代码:
var list = List.of("A", "B", "C");
var v = list[0];
在这种情况下,v
将是“A”
,代码等同于调用list.get(0)
。索引运算符无缝映射以获取和设置方法。我们也可以使用:
进行作业
var list = new ArrayList<>(List.of("A", "B", "C"));
var v = list[0];
list[0] = "1";
请注意,我必须将列表包装在ArrayList
中,因为List.of()
返回了无法解码的列表。但这是我涉及的部分。该代码很好。这个代码绝对令人惊讶:
var map = new HashMap<>(Map.of("Key", "Value"));
var key = map["Key"];
map["Key"] = "New Value";
是!
您会在歧管中读取有效的代码。索引操作员用于在地图中查找。请注意,地图具有put()方法,而不是设置方法。这是一种令人讨厌的不一致性,它用扩展方法固定了。然后,我们可以使用对象使用操作员在地图中查找。
关系和平等运营商
我们仍然有很多要介绍的内容。
if(vec3 > vec2) {
// …
}
默认情况下,这将不会编译。但是,如果我们将Comparable
接口添加到Vec
类中,这将按预期工作:
public class Vec implements Comparable<Vec> {
// …
public double magnitude() {
return Math.sqrt(x x + y y + z * z);
}
@Override
public int compareTo(Vec o) {
return Double.compare(magnitude(), o.magnitude());
}
}
这些>=, >, <, <=
比较操作员将通过调用compareTo
方法的预期工作。但是有一个大问题。您会注意到此列表中缺少==
和!=
操作员。在Java中,我们经常使用这些操作员进行指针比较,这在性能方面很有意义。我们不想改变爪哇固有的东西。为了避免这种情况,默认情况下,歧管不会超越这些操作员。
但是,我们可以实现ComparableUsing
接口,该接口是Comparable
接口的子接口。一旦这样做,==
和!=
将默认使用Equals方法。我们可以通过覆盖可以返回以下值之一的方法来覆盖这种行为:
-
CompareTo
-将使用==
和!=
的比较方法
-
Equals
(默认) - 将使用equals方法 -
Identity
-将使用指针比较,而Java中的规范
该接口还可以使我们覆盖compareToUsing(T, Operator)
方法。这类似于compareTo
方法,但让我们创建特定于操作员的行为,在某些边缘情况下可能很重要。
科学编码的单位表达式
请注意,单位表达式在歧管中是实验性的。但是它们是在这种情况下运营商超载的最有趣的应用程序之一。
单元表达式是一种新型的操作员,在执行强键入时会大大简化和增强科学编码。使用单元表达式,我们可以定义包含单位类型的数学表达式的符号。这为科学计算带来了新的清晰度和类型安全性。
例如,考虑一个距离计算,其中速度定义为每小时100英里。通过将速度(每小时英里)乘以时间(小时),我们可以得到距离:
Length distance = 100 mph * 3 hr;
Force force = 5kg * 9.807 m/s/s;
if(force == 49.035 N) {
// true
}
单位表达式允许我们表达数字值(或变量)及其相关单元。编译器检查单元的兼容性,防止不兼容的转换并确保准确的计算。此功能简化了科学代码,并可以轻松地实现强大的计算。
在引擎盖下,单位表达只是一个转换调用。 100 mph
表达式转换为:
VelocityUnit.postfixBind(Integer.valueOf(100))
此表达式返回速度对象。表达式3 hr
类似地与后缀方法绑定并返回一个时间对象。在这一点上,歧管Velocity
类具有times
方法,您回想起,这是一个操作员,并且在这两个结果上都被调用:
public Length times( Time t ) {
return new Length( toBaseNumber() * t.toBaseNumber(), LengthUnit.BASE, getDisplayUnit().getLengthUnit() );
}
请注意,该类具有接受不同对象类型的时代方法的多个超载版本。 Velocity
次Mass
将产生Momentum
。 Velocity
乘以Force
导致Power
。
即使在这个早期的实验阶段,也支持许多单位作为此软件包的一部分,check them out here。
您可能会在这里注意到很大的遗漏:货币。我很想有类似的东西:
var sum = 50 USD + 70 EUR;
如果您查看该代码,则应显而易见。我们需要一个汇率。没有汇率和可能的转换成本,这是没有意义的。财务计算的复杂性不能很好地转化为守则的当前状态。我怀疑这就是这仍然是实验性的原因。我很想知道如何优雅地解决类似的问题。
操作员超载的陷阱
尽管流形提供了强大的操作员超负荷功能,但要注意潜在的挑战和绩效考虑,这一点很重要。歧管的方法可能导致其他方法调用和对象分配,这可能会影响性能,尤其是在关键性环境中。考虑优化技术,例如减少不必要的方法调用和对象分配以确保有效的代码执行至关重要。
让我们查看此代码:
var n = x + y + z;
在表面上,它似乎有效而短。它物理转换为此代码:
var n = x.plus(y).plus(z);
这仍然很难发现,但请注意,为了创建结果,我们调用两种方法并至少分配两个对象。一种更有效的方法是:
var n = x.plus(y, z);
这是我们通常用于高性能矩阵计算的优化。您需要注意这一点,并了解如果性能很重要,则操作员在引擎盖下做什么。我不想暗示运营商天生较慢。实际上,它们的速度与方法调用一样快,但是有时调用的特定方法和分配量是不直觉的。
类型安全功能
以下与操作员过载有关的方面是第二个视频的一部分,因此我觉得它们是关于类型安全的广泛讨论的一部分。我最喜欢的一件事之一是它支持严格的打字和编译时间错误。对我来说,两者都代表了爪哇的核心精神。
越狱:类型安全反射
@JailBreak
是一项功能,可以授予班级内私人状态的访问。虽然听起来很糟糕,但@JailBreak
提供了一种更好的替代方法,可以使用传统的反射来访问私人变量。通过越狱课,我们可以无缝访问其私人状态,编译器仍在执行类型检查。从这个意义上讲,这是两个邪恶中的较小者。如果您要做一些可怕的事情(访问私人状态),那么至少要由编译器进行检查。
在以下代码中,值数组是私人的,但由于@JailBreak
注释,我们可以对其进行操作。此代码将打印“Ex0osed…”
:
@Jailbreak String exposedString = "Exposed...";
exposedString.value[2] = '0';
System.out.println(exposedString);
越狱也可以应用于静态字段和方法。但是,访问静态成员需要将null分配给该变量,这似乎是违反直觉的。但是,此功能为访问内部状态提供了一种更具控制和类型的安全方法,从而最大程度地减少了与使用反射相关的风险。
@Jailbreak String str = null;
str.isASCII(new byte[] { 111, (byte)222 });
最后,将歧管中的所有对象都注入了越狱方法。可以这样使用此方法(请注意,fastTime
是一个私有字段):
Date d = new Date();
long t = d.jailbreak().fastTime;
自注:执行方法参数类型
在Java中,某些API接受对象作为参数,即使可以使用更具体的类型。这可能会导致运行时潜在的问题和错误。但是,歧管引入了@Self
注释,该注释有助于强制执行作为参数传递的对象的类型。
通过用@Self
注释参数,我们明确说明仅接受指定的对象类型。这样可以确保类型的安全性,并防止意外使用不兼容的类型。通过此注释,编译器在开发过程中遇到了此类错误,减少了遇到生产问题的可能性。
让我以前的帖子中查看MySizeClass
:
public class MySizeClass {
int size = 5;
public int size() {
return size;
}
public void setSize(int size) {
this.size = size;
}
public boolean equals(@Self Object o) {
return o != null && ((MySizeClass)o).size == size;
}
}
注意,我添加了一个平等的方法,并用自我注释了该论点。如果删除自我注释,则该代码将编译:
var size = new MySizeClass();
size.equals("");
size.equals(new MySizeClass());
使用@Self
注释,字符串比较在编译期间将失败。
自动关键字:var的更强替代品
我不是var
关键字的忠实拥护者。我觉得它并没有简化,价格是将实施而不是界面编码的。我了解为什么Oracle的开发人员选择这条路。保守的决定是我发现Java如此吸引人的主要原因。歧管具有在这些约束之外工作的好处,它提供了一种更强大的替代品,称为auto
。 auto
可用于字段和方法返回值,使其比var更灵活。它提供了一种简洁而表达的方式来定义变量而不牺牲类型的安全性。
自动在使用元组时特别有用,这是本文中尚未讨论的功能。它允许优雅而简洁的代码,增强可读性和可维护性。您可以有效地将自动用作VAR
的倒入替换。最后
与歧管的运算符超载为Java带来表达和直观的数学符号,从而增强了代码的可读性和简单性。虽然Java并没有本地支持操作员的过载,但歧管开发人员可以实现类似的功能并在其代码中使用熟悉的操作员。通过利用歧管,我们可以编写更多的流动性和表达代码,尤其是在科学,数学和财务应用中。
歧管中的类型安全性提高使Java更加糟糕,就像Java一样。它使Java开发人员以强大的语言基础为基础,并采用更具表现力的类型保护编程范式。
我们应该将操作员重载添加到Java本身吗?
我不喜欢。我喜欢Java缓慢,稳定和保守。我也喜欢那种大胆而冒险的。这样,我可以在进行此方法时选择它是有意义的(例如,一个启动项目),但是为企业项目选择标准保守的Java。