java允许运算符重载,为什么Java不支持运算符重载?

Java不支持运算符重载 = 小白也能学编程

Java之所以不支持运算符重载,并不是如下原因:

  1. 会使JVM变得复杂、性能下降:君不见C++内置运算符重载的能力?C++的性能在任何时代秒杀Java相信没有争议。
  2. 便于静态分析、工具化等:一叶障目、不见泰山。运算符重载只是一种动态特性,动态语言的形式化静态分析方法已经有成熟的方法论。
  3. Java是面向对象语言:Ruby是比Java更彻底的面向对象的语言,然而它对运算符重载的支持非常优秀,在Ruby中一切都是对象,几乎一切都可以override。

java允许运算符重载,为什么Java不支持运算符重载?

不支持运算符重载的根本原因,是源自James Gosling设计Java的初衷:那就是要让Java的学习门槛足够低,这样才能让这个编程语言被更多的人使用,从而拥有最大的市场占有率。

java允许运算符重载,为什么Java不支持运算符重载?

Java诞生之前, 基本上是C/C++的天下。光C语言的一个指针,就吓退了多少莘莘学子?C++引入更多的动态特性:多态、多重继承、函数重载、函数重写、运算符重载、泛型……这更不知道让多少人望而却步!

正是在那样的大环境下,James Gosling才萌生了“开发一个小白都能上手”的编程语言的念头。

运算符重载的底层思想并不是面向对象

运算符重载的底层逻辑来自函数式编程。它的祖师爷是Lisp,一个“从来被模仿、从未被超越”的神级语言。

java允许运算符重载,为什么Java不支持运算符重载?

可以负责任地讲,如今流行的Python、Javascript、Typescript、Go、Ruby、Haskell、Scala、Groovy等,在动态高级特性上都是在不断模仿60多年前的Lisp。包括Java从诞生起就在鼓吹的垃圾回收等优点,全部都是“偷师”Lisp。有兴趣的小伙伴可以自行下载Lisp的发明者——John McCarthy老爷爷1960年发表的GC论文。

java允许运算符重载,为什么Java不支持运算符重载?

函数式语言的核心思想其实是数学。

说得更白话一点:通过数学表达式描述问题,而不是人肉模拟解答过程。问题描述完了,也就解决了——运行时处理执行细节。

说得更学院派一点:通过无状态的函数加以其他优化特性,将这些函数组件进行拼接。

看到这里,估计有不少人要来拍砖:运算符重载看起来那么复杂,明明可以定义方法或者函数来解决,除了装逼格,没有实用价值。

笔者这里回应一下:数学本来就不是普通大众擅长的,数学的目的就是用最简洁的方式来解决最复杂的问题。所以函数式语言从诞生之初,就没有想过要芸芸众生。它追求的是大道至简。

java允许运算符重载,为什么Java不支持运算符重载?

这里来看一个例子:计算一组数据(假设放在一个一维数组中)的标准差。

如果不采用函数式编程,采用通常的面向过程或者面向对象的编程范式,那么只能:

第一步,先通过循环体(for/foreach/while等),挨个遍历求出平均值mean;

第二步,再来一次循环,挨个求与mean的差值并平方,然后逐个累加得到平方合sumOfSquares;

第三步,对sumOfSquares调用平方根函数,求出最终值standardDeviation。

下面我们来进化一点:

有基本函数式编程概念的小伙伴可能会写出如下的简化范式(这里以Ruby为例):

mean = a.inject {|x,y| x+y } / a.size

sumOfSquares = a.map{|x| (x-mean)**2 }.inject{|x,y| x+y }

standardDeviation = Math.sqrt(sumOfSquares/(a.size-1))

但是真正的函数式编程高手是会这样写的:

第一步:写一个通用的数学意义上的复合函数(f(g(x)) = f*g(x))的表达:

module Functional

def apply(enum)

enum.map &self

end

alias | apply

def reduce(enum)

enum.inject &self

end

alias <= reduce

def compose(f)

if self.respond_to?(:arity) && self.arity == 1

lambda {|*args| self[f[*args]] }

else

lambda {|*args| self[*f[*args]] }

end

end

alias * compose

end

第二步:把计算标准差所需要的各个元素的数学表达列示好:

sum = lambda {|x,y| x+y } # A function to add two numbers

mean = (sum<=a)/a.size # Or sum.reduce(a) or a.inject(&sum)

deviation = lambda {|x| x-mean } # Function to compute difference from mean

square = lambda {|x| x*x } # Function to square a number

第三步:像写标准差的数学表达式一样,一步到位:

standardDeviation = Math.sqrt((sum<=square*deviation|a)/(a.size-1))

总结

Java之所以流行,并不是因为其语言设计得最优秀,相反地,在很多地方——比如泛型、Lambda、完全面向对象等设计上都存在不足。它的成功在于:扬长避短,把所有牛X的高级语言特性在一开始全部都抛弃,留一个最小核,然后通过营销,大规模地培养本语言阵营的程序员,建立各种各样的“轮子”,成就了巨无霸的生态;在站稳格局之后,慢慢地再逐步添加回来一些以前抛弃的其他语言的优秀特性——这是一种比较实用的策略,但是带来的恶果就是:历史包袱比较重,导致新特性很多时候是“半残”的。

回到运算符重载本身,对于高手,可以利用该特性写出极具“魔性”、接近数学语言的代码,这样的代码可以体现“极简之美”——但是,一个不利影响就是:数学不好的小伙伴,不容易看得懂,也很难体会其中蕴含的“数学之美”。

java允许运算符重载,为什么Java不支持运算符重载?

C语言如何支持C++重载?

这个问题描述其实有点问题,因为C++重载有两种场景:

  1. 函数重载
  2. 运算符重载

问题本身没有指明到底是哪种场景。现在就两个场景分别给出答案。

java允许运算符重载,为什么Java不支持运算符重载?

如何用C语言实现C++函数重载?

根据笔者的经验,共有3种方法可以实现:

  1. 用C语言实现一个C++编译器的对应子集, 后者自然可以支持重载;
  2. 用函数指针加上void指针类型参数强制类型转换,可以实现函数重载;
  3. 用宏加上可变参数,可以实现函数重载

java允许运算符重载,为什么Java不支持运算符重载?

如何用C语言实现C++运算符重载?

运算符在C语言中是保留字, 无法通过普通变通方法实现重载。只能用C语言实现一个C++编译器的对应子集, 后者自然可以支持重载。

用函数指针加上void指针类型参数强制类型转换,实现函数重载

用一个例子来说明:

typedef void (*funcOverride)(void *param);

void runFuncOverride(funcOverride f, void *param) {

f(param);

}

void func_with_int_param(void *iParam) {

int i = *(int *)iParam;

printf("int_param function is called, param is %d
", i);

}

void func_with_char_param(void *cParam) {

char c = *(char *)cParam;

printf("char_param function is called, param is %c
", c);

}

int i = 1;

char c='2';

runFuncOverride(func_with_int_param, &i);

runFuncOverride(func_with_char_param, &c);

输出结果为:

bint_param function is called, param is 1

char_param function is called, param is 2

这种方法有一个明显的劣势:

需要调用方事先指定函数指针挂接的实际调用的函数实体,即便是用变通的方式——将类型信息通过枚举类型或者字符串类型作为参数传递,也无法完美消除这个劣势。

根因是:运行时类型判断并未收纳于C语言标准规范中。

当然,一些C语言编译器可能会提供内置函数来实现该特性,但毕竟不是标准,无法满足跨平台的需求。比方说gcc就提供了__builtin_types_compatible_p和typeof这两个内置函数来做运行时类型判断。

用宏加上可变参数,实现函数重载

C语言支持可变参数,比方说prinf函数的原型如下:

int printf(const char *format, ...);

省略号表示参数为可变参数,而且C语言规定:省略号只能出现在函数形参的末尾,而且左边必须有普通的形参。

需要注意的是:对于宏没有上述限制。

C语言定义了一系列宏来完成可变参数函数参数的读取和使用:宏va_start、va_arg和va_end。

在ANSI C标准下,这些宏定义在stdarg.h中:

void va_start(va_list ap, last);//取第一个可变参数;

type va_arg(va_list ap, type);//获取当前位置参数值

void va_end(va_list ap);//将ap置为NULL

除此之外,还提供了一个非常有用的宏:__VA_ARGS__

这个宏直接引用可变参数列表。

有了上述前置知识,下面用一个例子来说明如何实现函数重载:

#define OneArgument(a) printf("One Argument func is called!
")

#define TwoArguments(a, b) printf("Two Arguments func is called!
")

#define MacroKernel(_1, _2, FUNC, ...) FUNC

#define Macro(...) MacroKernel(__VA_ARGS__, TwoArguments, OneArgument, ...)(__VA_ARGS__)

Macro(1);

Macro(2,3);

输出结果为:

One Argument func is called!

Two Arguments func is called!

上述代码估计有些小伙伴看了有点晕,现在做要点说明:

  1. Macro宏定义展开其实是:调用MacroKernel宏展开的函数实体,调用参数为可变参数,由Macro宏括号中的内容定义。
  2. __VA_ARGS__其实引用的就是Macro宏定义括号中的可变参数列表。
  3. MacroKernel宏定义中的_1,_2都是占位符,没有实际含义,作用是:保证在传给Macro不同数目参数时,使得FUNC指向对应的函数实体:Macro参数数目为1时,FUNC指向OneArgument函数;参数数目为2时,FUNC指向TwoArguments函数。

下面来看看实际的展开效果:

Macro(1)

=>MacroKernel(1, TwoArguments, OneArgument, ...)(1)

=>OneArgument(1)

Macro(2, 3)

=>MacroKernel(2, 3, TwoArguments, OneArgument, ...)(2, 3)

=>TwoArguments(2, 3)

C++运算符重载的设计意义大吗?

意义比较大,它是为了让代码封装行和易用性的一种保障

举个简单例子,如何把一个对象用cout打印到控制台?如果没有重载操作符,会很麻烦

AI算法工程师,欢迎拍砖

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 xxx@163.com 举报,一经查实,本站将立刻删除。

发表评论

登录后才能评论