当前位置:网站首页 > Java基础 > 正文

java编程基础教学书籍阅读



《Thinking in Java》其实像这样的小错误挺多的,作者喜欢无视规范对术语的定义而是添上个人色彩的渲染,所以我不喜欢这系列书。

这里中文版翻译是忠于原文的,有错不怪翻译而只能怪原文。题目截图的那页的原文请参考

@Intopass

的回答。

简单说:

  • 从Java语言层面看,构造器不是静态方法。事实上规范专门规定了构造器不是方法。
  • 从JVM层面看,构造器属于特殊的初始化方法,但仍然不被归类为静态方法。
  • 无论从哪个层面看,构造器都是可以静态分派的(statically dispatched)。但要特别注意的是,并非所有可以静态分派的代码都是静态方法,这是规范里有定义的术语。
  • 无论从哪个层面看,构造器都要接收从调用方传入的隐藏this参数。Java的静态方法不需要接收该参数,而实例方法要接收该参数。这方面构造器更接近于可以静态分派的实例方法,例如私有实例方法。这就是为什么从构造器可以调用this上的实例方法。
  • 书中的Dog例子,确实当Dog被首次实例化或其静态方法/静态变量被首次访问时,会触发Dog类的初始化。但构造器在此并不可以归类到静态方法的一侧。事实上构造器并不能被单独调用,而必须用在new表达式里,此时触发类初始化的正是new,而不是对构造器的调用——new与构造器调用是两码事。

我以前在博客上发过一篇详细讲解,请跳传送门:

实例构造器是不是静态方法?


有啥相关问题请先看看这篇讲解。

思维不要局限在面向对象。

  1. 先弄清问题是什么问题;
  2. 别找现成解决方案;
  3. 考虑解决此问题需哪些输入,应有哪些输出;
  4. 评估自己是否有能力解决中间逻辑;
  5. 中间逻辑是否太大、太复杂,如果是,能否拆,变成小问题;
  6. 小问题自己能否解决,评估可以,跳过,否则继续拆,直到能解决为止;
  7. 考虑若问题扩展,当前拆分能否应对;如不能,为什么,是不是引入新问题,如果是,针对新问题用同样方式分析,直到解决;
  8. 如解决不了,考虑回溯主线,从根改起;
  9. 如回溯改主线也解决不了,裁剪需求,取舍;
  10. 整理前面结论,得出一个基本能解决当前问题和扩展问题的理论方案;
  11. 考虑哪些能用现成的,且简单、经济;
  12. 考虑哪些部分可复用到其他领域或业务,挑出来
  13. 考虑用哪种形式包装它们:库、框架、中间件、API

一次性拆分得恰到好处不可能,没见到谁做到过。

这是三个问题,我的答案不一定好,只是多年的一些思考。

1、怎么培养面向对象编程抽象思维?

这方面的能力,需要有一定的悟性,我尝试对我团队的人在这一方面进行培养,初见成效。

培养抽象思维的能力,根据现有的问题,冷静的思考其存在的本质,追根溯源,学会对本质进行定义,找到合适的名字,确定其定义的边界和解释。当有了本质的思考之后,尝试用本质去套用和推演现遇到的问题和场景,如果问题不能解,则说明抽象和思考得不够好;如果问题能解,则可能在当下满足要求。本质思考是持续的,变化的,多元的,随着时间的变化,问题也会变化,思考的加深,可能找到更深层次的本质内在的东西而重新得到定义。如果思考陷入多元的本质问题,则可能需要学会分类和组合。

有了定义之后,实现方面,要注意的是,将定义工程化,用程序去定义它,而不是一个简单的文档或者一句口头的话,这一点非常重要。你也许会发现,很多程序中,都只是一个定义,比如标记接口,有了定义,就可以进行相应的领域的处理。

具体的例子我不举了,其实我们有很多好的抽象的例子,一些源代码中,多关注一些抽象类、接口的定义,看看他们如何取名字、如何组织代码结构,如何抽象的,反复推敲别人这样设计背后的原因。

2、如何站在宏观角度去思考架构设计?

这个问题涉及到角度,架构师层次不同,看问题的角度也会不一样,所以,这个问题实际上是架构师的视野问题。

宏观这个词是相对的,宏观相对于微观,宏观的层次到底在哪里,是一个可以探讨的问题,视野决定了高度。从国际环境到国家战略、从国家战略到行业方向、从行业方向到企业目标、从企业目标到企业架构……,每一层都结合起来看,逐层拆解,结合各层的环境因素,审时度势,确定架构设计和目标。

高层的视野和接地气本身是一对矛盾,如何平衡好这对矛盾,为什么架构师要从底层做起,原因是因为架构师能够敏锐的通过一个小的问题,可见一斑,结合高层的思考形成架构方案。如果没有这些基层储备,则架构目标容易不着边际,飘于九霄之外,容易跑偏。

关于如何提高视野,其实简单直接的办法就是比你高层的人沟通,沟通不一定是带着问题去的,闲聊的时候,往往会产生不一样的效果。也不一定是同岗位的人,不同岗位的人,对于视野的开拓更加明显。比如你是一个技术架构师,你可以和财务、和市场人员、和客户、和CXO等。

3、对业务的抽象拆分如何做到恰到好处?

抽象的程度最起码的要求是解决现在的问题,这是最低级的架构方案,什么是恰到好处,这个多变的世界,我们一般往后看两年(具体几年可能不一样,这个就是你给自己一个度的定义),这需要有一定的前瞻性,架构都是面向未来做的架构的。所以根据前面说的,要审时度势,根据形势能判断未来两年内的发展情况。两年时间,我们要做些什么,什么时候做,怎么做,谁来做,这是要思考的,也是一个谋划和变化的过程,没有固定的标准。

以前已经回答过这问题了,这本书十多年前的,只适合买来装B。对那些对java半懂不懂的人,乍一看有点用。但像我以前系统看完疯狂java讲义,就会觉得java编程思想帮助不太,而且知识很陈旧,国外同事们都说早没人看这么老的书了

国内最新翻译是第四版 写的是jdk5的知识 相较于目前来说是落后了点 作者也新开了一本书 on java 8 英语能力可以的直接可以撸了 下面是作者github地址

BruceEckel/OnJava8-Examples

on java 8 地址,需要翻墙,恩 需要购买:

play.google.com/store/b

作者博客

On Java 8 And The Concurrent Python Developer Retreat

以下针对第四版

对于工作帮助,这看你是萌新还是大佬了, 大佬可以pass, 萌新(已经对Java有初步的认识,可以进行基本工作)还是看看吧 哦 萌新我推荐 Think in java 4th+Effective Java+ 设计模式 套餐

萌新在参与一个新的项目的时候心里没一点B数 能够把整个项目带歪....

像Java核心技术 它教你的是how 而Think in java和Effective Java 教你的是why 授人以鱼不如授人以渔

有过项目经验的可以看看看跟自己的经验印证一下

多说一点。初学java知道个大概就可以了。异常,io,注解等这种粗略看一下,因为在你未来半年可能都不会能用的到,等你用的到的时候在认真学。我当时在这方面下的苦工完全没用,没有应用场景,就书上的小demo敲完以后一年过去基本全忘。

初学Java重在理解语言复杂的部分(虽然没啥复杂的知识点)

————————分割线—————————

本人计算机本科大三,大二的时候看完了编程思想。当初也是将这本书作为入门书籍。回过头来看,这本书完全不适合新手,看官方文档都比这本书好使。因为编程思想中注重的是思想,里面的代码示例过于复杂,设计模式都在其中。当时看示例代码的时候往往看的云里雾里。别的书,一般一个知识点只用一个简要的代码块去实现,让你明白这个知识点就行。但在编程思想这本书中,往往本章第一个定义的类或者接口在20多页后的示例中他拿出来用,你看的的示例中突然冒出来一个在你看来根本没被定义过的类,过程很痛苦。初看这本书比看算法书还难。对了里面会出现我到现在还不理解的设计思想

总结一下:初学者看编程思想得不偿失,事倍功半。那些吹初学者看这本书的人我只能说nc秀优越感。初学者适合java核心技术卷1。还有就是个人建议初学的时候最好每个例子都敲一敲。要不然这种类似于文档的书学完啥也记不住。

java进阶我觉得也是看jdk doc 和源码。没必要在从头到尾看这本书。当然零零碎碎查阅知识点还是不错的。

采纳的话,点个赞吧,蟹蟹

都错了,书里第一点说的构造器并不是我们所理解的那个实例化构造方法!!!

除了实例化构造方法,还有一个不为人知的class(静态)构造方法。

各位答主为啥没人往这个点上说呢?

翻译过来的书,无论是中文书翻译成英文,还是英文书翻译成中文,有些时候字面翻译真的很难精确表达其原有的语意(要么得添加一些补充说明),而在计算机领域,很多经典书都是英文书翻译成中文(谁叫老外发展的早呢)。

题主这里就是一个典型了。

构造方法的两种类型

在jvm规范中描述了一些特殊的方法:

jvm规范描述的特殊方法

有两个初始化方法,分别是instance初始化方法class初始化方法

instance初始化方法:其实就是开发者经常说的实例化构造方法。它做的工作是实例字段的初始化工作。

class初始化方法:其实是class对象的构造方法,可以理解为class(静态)构造方法。它做的是静态字段的初始化工作。

这个方法不为人所知的原因,是因为它不需要开发者去写这个方法,这个方法是java编译器自动生成的

换句话说,她一直就在那里,只不过你一直都没有发现罢了(人zha啊~)。

《thinking in java》里指的是哪个构造方法

其实知道上面的点以后就很好理解《thinking in java》原文想要表达的意思了(原文参考 @Intopass 的截图),也就知道中文译版要表达的意思。

它里面的constructor指的并不是实例化构造方法!而是class(静态)构造方法!!!

我们知道java里每个对象都会对应有且仅有一个class对象,也就是class对象是单例

所以第一次创建Dog对象的时候,得先把其对应class对象创建出来(这一步是在类加载过程里面做的),后续再创建多少个Dog对象,就不需要再创建对应的class对象了,因为已经创建过了啊~

所以class对象是单例的,其class(静态)构造方法也就只会像书里说的,最多被执行一次

class构造方法为什么应该是静态的

刚才说了,class构造方法做的事情是对静态字段的初始化,也就是静态字段的赋值操作。

所以它虽然在编译后的字节码文件(java7之前)里没有显示的被static修饰,但是我们知道java编程语言里,静态方法不能访问实例字段

而class构造方法只是初始化静态字段,无关实例化字段,静态修饰更符合语意

所以它本质上是静态方法,也就是《thinking in java》中所想要表达的意思。

java7已经要求显示加上static修饰符

不过,不知道最新版的《thinking in java》有没有同步更新java7的内容,java7开始要求在class构造方法显示加上static修饰符了(见上方jvm规范截图)。

the requirement for ACC_STATIC was introduced in JAVA SE7.

我加上一些实锤的证据吧。

我用java9编译某个java文件(里面有一个静态字段age并且赋予了初始值),得到其部分字节码如下:

public class Test { public static int age = 8; ... }
java编译器自动生成的class(静态)构造方法,类加载的时候被执行

可以看到flags属性有个ACC_STATIC,对应java编程语言的static修饰符

字节码反推,该class(静态)构造方法类似这样:

static void <clinit>(){ ... }

仅有一个static修饰符,当然,这个在java7之前是没有的。

再看实例化构造方法:

实例化构造方法

可以看到flags属性,实例化构造方法经过编译后,也还是只有一个ACC_PUBLIC标志,对应java编程语言中的public修饰符。

白嫖可以,来个点赞三连好嘛?

听说双击屏幕有惊喜??

延伸

java编程语言里有些东西其实是语法糖,经过java编译器处理后的class文件其实没有差别(比如代码块)。

如何理解java中的代码块:

Java静态代码块运行疑惑?

char类型和String类型相关回答:

jdk9为何要将String的底层实现由char[]改成了byte[]?Java中关于Char存储中文到底是2个字节还是3个还是4个?

如何理解java中的this:

关于java中的this用法,一直搞不清楚,有没有人汇总一下?

先说结论:学习java编程,任何阶段都不建议去阅读《java编程思想》。

现在java 8都已经被spring 3、Jenkins等平台弃用了。而《java编程思想》还是在使用jdk 1.5版本在教学。里面很多知识点都发生了变化。不值得再去花费时间阅读。

如果你对《java编程思想》或者《think in java》感兴趣,不如花点时间去读一读《think in java》作者最新出版的《On java》,不但继承了think in java的经典,同时还对知识点进行了更新,基于java 8,11,17很多新特性做了讲解。

首先说明我不是云建议,而是此时此刻正耐着性子压着火在读《java编程思想》。不建议的原因如下:

  1. 太过陈旧。目前我看的第四版,讲解的是基于Java SE 5.0来讲解的。目前java 17都出来了。很多新的特性都没讲到,讲解的非常陈旧和基础。
  2. 书写思路有问题。书中反复和c++进行比较,来讲解java的思路。实际上java应该和c#来做比较,而不是c++。他们俩思路不在一条线上,去这样比较反而让做企业级工程的开发者云里雾里。
  3. 性价比非常低。这么基础的东西洋洋洒洒写了800页。很多东西事无巨细的讲得太过拖沓,如果是面向初学者,应该讲得浅而广,这样初学者很容易入门,并且成就感满满。如果面向高级编程者,完全不用事无巨细。这样初学者基本上进去容易出来难,高手又容易被基础所累而找不到重点。
  4. 不知道是翻译问题还是书本身的问题,常常读过以后不知道他表达的到底是啥。我说的不仅仅是知识点,甚至他要描述的案例。
  5. 书写的逻辑有问题。我认为如果技术类书写案例,应该先提出问题,让读书的人大概知道你在解决什么问题,甚至构建自己的思路,然后再提供代码。这本书恰恰相反,给你给出一大堆代码,然后后面浅浅的解释下代码的某些含义。经常看过代码一头雾水,然后再去后面看文字描述的意图,然后还得回来看代码。
  6. 排版混乱。代码文字基本上用同样的字体和颜色罗列,你经常抓不住重点。
  7. 代码交叉使用,阅读困难。经常遇到你在第21章的代码用了第8章的某个类。而第八章可能是三五个月前你看的内容,早就忘得精光。

当然也有优点。

1.真厚,磨性子。算是当做修行了。

2.事无巨细。当做查漏补缺,但是最新的特性没有。

3.用了大量的设计模式,强迫或者半强迫你会了解很多设计模式。

我比较推荐的两本学习java的书。

来吧,对比下基本上的排版。你就明白读java编程思想有多痛苦。

java编程思想的。

head first系列

java核心技术

不推荐,非常不推荐!!!

这本书给我的感觉已经不是传播知识的载体了,而是变成了彰显国内某些Java程序员身份的工具。在这些高逼格程序人员面前,你要是说”没读过这本书“,“这本书落伍了”,你都会被鄙视的不敢出门!

首先,这本书太老了,已经Java17LTS了,Java8都要被放弃了,还推荐给新人就是害人。

其次,这本书的中文翻译能逼着你去读英文原版,Think in Java...

然后,为什么不去读同作者的新书 On Java 8 呢?

最后,Java已经演化的够庞大了,初学者已经学不过来各个原理和理论了。求各位大神以后还是推荐些偏应用和实践的书吧,可怜可怜孩子吧,让孩子先入个门先!原理慢慢的就懂了!

我们是否一样?

估计很多小伙伴(也包括我自己)都有这种情况,在自学Java语言看书时,关于枚举enum这一块的知识点可能都有点 “轻敌” ,觉得这块内容非常简单,一带而过,而且在实际写代码过程中也不注意运用。

是的,我也是这样!直到有一天我提的代码审核没过,被技术总监一顿批,我才重新拿起了《Java编程思想》,把枚举这块的知识点重新又审视了一遍。

为什么需要枚举

常量定义它不香吗?为啥非得用枚举?

举个栗子,就以B站上传视频为例,视频一般有三个状态:草稿审核发布,我们可以将其定义为静态常量

public class VideoStatus { public static final int Draft = 1; //草稿 public static final int Review = 2; //审核 public static final int Published = 3; //发布 }

对于这种单值类型的静态常量定义,本身也没错,主要是在使用的地方没有一个明确性的约束而已,比如:

void judgeVideoStatus( int status ) { ... }

比如这里的 judgeVideoStatus 函数的本意是传入 VideoStatus 的三种静态常量之一,但由于没有类型上的约束,因此传入任意一个int值都是可以的,编译器也不会提出任何警告。

但是在枚举类型出现之后,上面这种情况就可以用枚举严谨地去约束,比如用枚举去定义视频状态就非常简洁了:

public enum VideoStatus { Draft, Review, Published }

而且主要是在用枚举的地方会有更强的类型约束

// 入参就有明确类型约束 void judgeVideoStatus( VideoStatus status ) { ... }

这样在使用 judgeVideoStatus 函数时,入参类型就会受到明确的类型约束,一旦传入无效值,编译器就会帮我们检查,从而规避潜在问题。

除此之外,枚举在扩展性方面比普常量更方便、也更优雅。

重新系统认识一下枚举

还是拿前文《答应我,别再if/else走天下了可以吗》中的那个例子来说:比如,在后台管理系统中,肯定有用户角色一说,而且角色一般都是固定的,适合定义成一个枚举:

public enum UserRole { ROLE_ROOT_ADMIN, // 系统管理员 ROLE_ORDER_ADMIN, // 订单管理员 ROLE_NORMAL // 普通用户 }

接下来我们就用这个UserRole为例来说明枚举的所有基本用法

UserRole role1 = UserRole.ROLE_ROOT_ADMIN; UserRole role2 = UserRole.ROLE_ORDER_ADMIN; UserRole role3 = UserRole.ROLE_NORMAL; // values()方法:返回所有枚举常量的数组集合 for ( UserRole role : UserRole.values() ) { System.out.println(role); } // 打印: // ROLE_ROOT_ADMIN // ROLE_ORDER_ADMIN // ROLE_NORMAL // ordinal()方法:返回枚举常量的序数,注意从0开始 System.out.println( role1.ordinal() ); // 打印0 System.out.println( role2.ordinal() ); // 打印1 System.out.println( role3.ordinal() ); // 打印2 // compareTo()方法:枚举常量间的比较 System.out.println( role1.compareTo(role2) ); //打印-1 System.out.println( role2.compareTo(role3) ); //打印-2 System.out.println( role1.compareTo(role3) ); //打印-2 // name()方法:获得枚举常量的名称 System.out.println( role1.name() ); // 打印ROLE_ROOT_ADMIN System.out.println( role2.name() ); // 打印ROLE_ORDER_ADMIN System.out.println( role3.name() ); // 打印ROLE_NORMAL // valueOf()方法:返回指定名称的枚举常量 System.out.println( UserRole.valueOf( "ROLE_ROOT_ADMIN" ) ); System.out.println( UserRole.valueOf( "ROLE_ORDER_ADMIN" ) ); System.out.println( UserRole.valueOf( "ROLE_NORMAL" ) );

除此之外,枚举还可以用于switch语句中,而且意义更加明确:

UserRole userRole = UserRole.ROLE_ORDER_ADMIN; switch (userRole) { case ROLE_ROOT_ADMIN: // 比如此处的意义就非常清晰了,比1,2,3这种数字好! System.out.println("这是系统管理员角色"); break; case ROLE_ORDER_ADMIN: System.out.println("这是订单管理员角色"); break; case ROLE_NORMAL: System.out.println("这是普通用户角色"); break; }

除此之外,枚举还可以用于switch语句中,而且意义更加明确:

UserRole userRole = UserRole.ROLE_ORDER_ADMIN; switch (userRole) { case ROLE_ROOT_ADMIN: // 比如此处的意义就非常清晰了,比1,2,3这种数字好! System.out.println("这是系统管理员角色"); break; case ROLE_ORDER_ADMIN: System.out.println("这是订单管理员角色"); break; case ROLE_NORMAL: System.out.println("这是普通用户角色"); break; }

自定义扩充枚举

上面展示的枚举例子非常简单,仅仅是单值的情形,而实际项目中用枚举往往是多值用法。

比如,我想扩充一下上面的UserRole枚举,在里面加入 角色名 – 角色编码 的对应关系,这也是实际项目中常用的用法。

这时候我们可以在枚举里自定义各种属性、构造函数、甚至各种方法:

public enum UserRole { ROLE_ROOT_ADMIN( "系统管理员", 000000 ), ROLE_ORDER_ADMIN( "订单管理员",  ), ROLE_NORMAL( "普通用户",  ), ; // 以下为自定义属性 private final String roleName; //角色名称 private final Integer roleCode; //角色编码 // 以下为自定义构造函数 UserRole( String roleName, Integer roleCode ) { this.roleName = roleName; this.roleCode = roleCode; } // 以下为自定义方法 public String getRoleName() { return this.roleName; } public Integer getRoleCode() { return this.roleCode; } public static Integer getRoleCodeByRoleName( String roleName ) { for( UserRole enums : UserRole.values() ) { if( enums.getRoleName().equals( roleName ) ) { return enums.getRoleCode(); } } return null; } }

从上述代码可知,在enum枚举类中完全可以像在普通Class里一样声明属性、构造函数以及成员方法。

枚举 + 接口 = ?

比如在我的前文《答应我,别再if/else走天下了可以吗》中讲烦人的if/else消除时,就讲过如何通过让枚举去实现接口来方便的完成。

这地方不妨再回顾一遍:

什么角色能干什么事,这很明显有一个对应关系,所以我们首先定义一个公用的接口RoleOperation,表示不同角色所能做的操作:

public interface RoleOperation { String op(); // 表示某个角色可以做哪些op操作 }

接下来我们将不同角色的情况全部交由枚举类来做,定义一个枚举类RoleEnum,并让它去实现RoleOperation接口:

public enum RoleEnum implements RoleOperation { // 系统管理员(有A操作权限) ROLE_ROOT_ADMIN { @Override public String op() { return "ROLE_ROOT_ADMIN:" + " has AAA permission"; } }, // 订单管理员(有B操作权限) ROLE_ORDER_ADMIN { @Override public String op() { return "ROLE_ORDER_ADMIN:" + " has BBB permission"; } }, // 普通用户(有C操作权限) ROLE_NORMAL { @Override public String op() { return "ROLE_NORMAL:" + " has CCC permission"; } }; }

这样,在调用处就变得异常简单了,一行代码就行了,根本不需要什么if/else

public class JudgeRole { public String judge( String roleName ) { // 一行代码搞定!之前的if/else灰飞烟灭 return RoleEnum.valueOf(roleName).op(); } }

而且这样一来,以后假如我想扩充条件,只需要去枚举类中加代码即可,而不用改任何老代码,非常符合开闭原则

枚举与设计模式

什么?枚举还能实现设计模式?

是的!不仅能而且还能实现好几种!

1、单例模式

public class Singleton { // 构造函数私有化,避免外部创建实例 private Singleton() { } //定义一个内部枚举 public enum SingletonEnum{ SEED; // 唯一一个枚举对象,我们称它为“种子选手”! private Singleton singleton; SingletonEnum(){ singleton = new Singleton(); //真正的对象创建隐蔽在此! } public Singleton getInstnce(){ return singleton; } } // 故意外露的对象获取方法,也是外面获取实例的唯一入口 public static Singleton getInstance(){ return SingletonEnum.SEED.getInstnce(); // 通过枚举的种子选手来完成 } }

2、策略模式

这个也比较好举例,比如用枚举就可以写出一个基于策略模式的加减乘除计算器

public class Test { public enum Calculator { ADDITION { public Double execute( Double x, Double y ) { return x + y; // 加法 } }, SUBTRACTION { public Double execute( Double x, Double y ) { return x - y; // 减法 } }, MULTIPLICATION { public Double execute( Double x, Double y ) { return x * y; // 乘法 } }, DIVISION { public Double execute( Double x, Double y ) { return x/y; // 除法 } }; public abstract Double execute(Double x, Double y); } public static void main(String[] args) { System.out.println( Calculator.ADDITION.execute( 4.0, 2.0 ) ); // 打印 6.0 System.out.println( Calculator.SUBTRACTION.execute( 4.0, 2.0 ) ); // 打印 2.0 System.out.println( Calculator.MULTIPLICATION.execute( 4.0, 2.0 ) ); // 打印 8.0 System.out.println( Calculator.DIVISION.execute( 4.0, 2.0 ) ); // 打印 2.0 } }

专门用于枚举的集合类

我们平常一般习惯于使用诸如:HashMapHashSet等集合来盛放元素,而对于枚举,有它专门的集合类:EnumSetEnumMap

1、EnumSet

EnumSet 是专门为盛放枚举类型所设计的 Set 类型。

还是举例来说,就以文中开头定义的角色枚举为例:

public enum UserRole { ROLE_ROOT_ADMIN, // 系统管理员 ROLE_ORDER_ADMIN, // 订单管理员 ROLE_NORMAL // 普通用户 }

比如系统里来了一批人,我们需要查看他是不是某个角色中的一个:

// 定义一个管理员角色的专属集合 EnumSet<UserRole> userRolesForAdmin = EnumSet.of( UserRole.ROLE_ROOT_ADMIN, UserRole.ROLE_ORDER_ADMIN ); // 判断某个进来的用户是不是管理员 Boolean isAdmin( User user ) { if( userRoles.contains( user.getUserRole() ) ) return true; return false; }

2、EnumMap

同样,EnumMap 则是用来专门盛放枚举类型为keyMap 类型。

比如,系统里来了一批人,我们需要统计不同的角色到底有多少人这种的话:

Map<UserRole,Integer> userStatisticMap = new EnumMap<>(UserRole.class); for ( User user : userList ) { Integer num = userStatisticMap.get( user.getUserRole() ); if( null != num ) { userStatisticMap.put( user.getUserRole(), num+1 ); } else { userStatisticMap.put( user.getUserRole(), 1 ); } }

EnumMap可以说非常方便了。

总 结

小小的枚举就玩出这么多的花样,不过好在探索和总结的过程还挺有意思的,也复习了很多知识,慢慢来吧。


作者:CodeSheep程序羊

链接:imooc.com/article/30206

来源:慕课网

本文原创发布于慕课网 ,转载请注明出处,谢谢合作

工具人

曾几何时,对于Java的序列化的认知一直停留在:「实现个Serializbale接口」不就好了的状态,直到 ...

所以这次抽时间再次重新捧起了尘封已久的《Java编程思想》,就像之前梳理《枚举部分知识》一样,把「序列化和反java编程基础教学书籍阅读序列化」这块的知识点又重新审视了一遍。

序列化是干啥用的?

序列化的原本意图是希望对一个Java对象作一下“变换”,变成字节序列,这样一来方便持久化存储到磁盘,避免程序运行结束后对象就从内存里消失,另外变换成字节序列也更便于网络运输和传播,所以概念上很好理解:

  • 序列化:把Java对象转换为字节序列。
  • 反序列化:把字节序列恢复为原先的Java对象。

而且序列化机制从某种意义上来说也弥补了平台化的一些差异,毕竟转换后的字节流可以在其他平台上进行反序列化来恢复对象。

事情就是那么个事情,看起来很简单,不过后面的东西还不少,请往下看。

对象如何序列化?

然而Java目前并没有一个关键字可以直接去定义一个所谓的“可持久化”对象。

对象的持久化和反持久化需要靠程序员在代码里手动显式地进行序列化和反序列化还原的动作。

举个例子,假如我们要对Student类对象序列化到一个名为student.txt的文本文件中,然后再通过文本文件反序列化成Student类对象:

1、Student类定义

public class Student implements Serializable { private String name; private Integer age; private Integer score; @Override public String toString() { return "Student:" + 'n' + "name = " + this.name + 'n' + "age = " + this.age + 'n' + "score = " + this.score + 'n' ; } // ... 其他省略 ... }

2、序列化

public static void serialize( ) throws IOException { Student student = new Student(); student.setName("CodeSheep"); student.setAge( 18 ); student.setScore( 1000 ); ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) ); objectOutputStream.writeObject( student ); objectOutputStream.close(); System.out.println("序列化成功!已经生成student.txt文件"); System.out.println("=============================================="); }

3、反序列化

public static void deserialize( ) throws IOException, ClassNotFoundException { ObjectInputStream objectInputStream = new ObjectInputStream( new FileInputStream( new File("student.txt") ) ); Student student = (Student) objectInputStream.readObject(); objectInputStream.close(); System.out.println("反序列化结果为:"); System.out.println( student ); }

4、运行结果

控制台打印:

序列化成功!已经生成student.txt文件 ============================================== 反序列化结果为: Student: name = CodeSheep age = 18 score = 1000

Serializable接口有何用?

上面在定义Student类时,实现了一个Serializable接口,然而当我们点进Serializable接口内部查看,发现它竟然是一个空接口,并没有包含任何方法!

试想,如果上面在定义Student类时忘了加implements Serializable时会发生什么呢?

实验结果是:此时的程序运行会报错,并抛出NotSerializableException异常:

我们按照错误提示,由源码一直跟到ObjectOutputStreamwriteObject0()方法底层一看,才恍然大悟:

如果一个对象既不是字符串数组枚举,而且也没有实现Serializable接口的话,在序列化时就会抛出NotSerializableException异常!

哦,我明白了!

原来Serializable接口也仅仅只是做一个标记用!!!

它告诉代码只要是实现了Serializable接口的类都是可以被序列化的!然而真正的序列化动作不需要靠它完成。

serialVersionUID号有何用?

相信你一定经常看到有些类中定义了如下代码行,即定义了一个名为serialVersionUID的字段:

private static final long serialVersionUID = -L;

你知道这句声明的含义吗?为什么要搞一个名为serialVersionUID的序列号?

继续来做一个简单实验,还拿上面的Student类为例,我们并没有人为在里面显式地声明一个serialVersionUID字段。

我们首先还是调用上面的serialize()方法,将一个Student对象序列化到本地磁盘上的student.txt文件:

public static void serialize() throws IOException { Student student = new Student(); student.setName("CodeSheep"); student.setAge( 18 ); student.setScore( 100 ); ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) ); objectOutputStream.writeObject( student ); objectOutputStream.close(); }

接下来我们在Student类里面动点手脚,比如在里面再增加一个名为studentID的字段,表示学生学号:

这时候,我们拿刚才已经序列化到本地的student.txt文件,还用如下代码进行反序列化,试图还原出刚才那个Student对象:

public static void deserialize( ) throws IOException, ClassNotFoundException { ObjectInputStream objectInputStream = new ObjectInputStream( new FileInputStream( new File("student.txt") ) ); Student student = (Student) objectInputStream.readObject(); objectInputStream.close(); System.out.println("反序列化结果为:"); System.out.println( student ); }

运行发现报错了,并且抛出了InvalidClassException异常:

这地方提示的信息非常明确了:序列化前后的serialVersionUID号码不兼容!

从这地方最起码可以得出两个重要信息:

  • 1、serialVersionUID是序列化前后的唯一标识符
  • 2、默认如果没有人为显式定义过serialVersionUID,那编译器会为它自动声明一个!

第1个问题: serialVersionUID序列化ID,可以看成是序列化和反序列化过程中的“暗号”,在反序列化时,JVM会把字节流中的序列号ID和被序列化类中的序列号ID做比对,只有两者一致,才能重新反序列化,否则就会报异常来终止反序列化的过程。

第2个问题: 如果在定义一个可序列化的类时,没有人为显式地给它定义一个serialVersionUID的话,则Java运行时环境会根据该类的各方面信息自动地为它生成一个默认的serialVersionUID,一旦像上面一样更改了类的结构或者信息,则类的serialVersionUID也会跟着变化!

所以,为了serialVersionUID的确定性,写代码时还是建议,凡是implements Serializable的类,都最好人为显式地为它声明一个serialVersionUID明确值!

当然,如果不想手动赋值,你也可以借助IDE的自动添加功能,比如我使用的IntelliJ IDEA,按alt + enter就可以为类自动生成和添加serialVersionUID字段,十分方便:

两种特殊情况

  • 1、凡是被static修饰的字段是不会被序列化的
  • 2、凡是被transient修饰符修饰的字段也是不会被序列化的

对于第一点,因为序列化保存的是对象的状态而非类的状态,所以会忽略static静态域也是理所应当的。

对于第二点,就需要了解一下transient修饰符的作用了。

如果在序列化某个类的对象时,就是不希望某个字段被序列化(比如这个字段存放的是隐私值,如:密码等),那这时就可以用transient修饰符来修饰该字段。

比如在之前定义的Student类中,加入一个密码字段,但是不希望序列化到txt文本,则可以:

这样在序列化Student类对象时,password字段会设置为默认值null,这一点可以从反序列化所得到的结果来看出:

序列化的受控和加强

约束性加持

从上面的过程可以看出,序列化和反序列化的过程其实是有漏洞的,因为从序列化到反序列化是有中间过程的,如果被别人拿到了中间字节流,然后加以伪造或者篡改,那反序列化出来的对象就会有一定风险了。

毕竟反序列化也相当于一种 “隐式的”对象构造 ,因此我们希望在反序列化时,进行受控的对象反序列化动作。

那怎么个受控法呢?

答案就是: 自行编写readObject()函数,用于对象的反序列化构造,从而提供约束性。

既然自行编写readObject()函数,那就可以做很多可控的事情:比如各种判断工作。

还以上面的Student类为例,一般来说学生的成绩应该在0 ~ 100之间,我们为了防止学生的考试成绩在反序列化时被别人篡改成一个奇葩值,我们可以自行编写readObject()函数用于反序列化的控制:

private void readObject( ObjectInputStream objectInputStream ) throws IOException, ClassNotFoundException { // 调用默认的反序列化函数 objectInputStream.defaultReadObject(); // 手工检查反序列化后学生成绩的有效性,若发现有问题,即终止操作! if( 0 > score || 100 < score ) { throw new IllegalArgumentException("学生分数只能在0到100之间!"); } }

比如我故意将学生的分数改为101,此时反序列化立马终止并且报错:

对于上面的代码,有些小伙伴可能会好奇,为什么自定义的privatereadObject()方法可以被自动调用,这就需要你跟一下底层源码来一探究竟了,我帮你跟到了ObjectStreamClass类的最底层,看到这里我相信你一定恍然大悟:

又是反射机制在起作用!是的,在Java里,果然万物皆可“反射”(滑稽),即使是类中定义的private私有方法,也能被抠出来执行了,简直引起舒适了。

单例模式增强

一个容易被忽略的问题是:可序列化的单例类有可能并不单例

举个代码小例子就清楚了。

比如这里我们先用java写一个常见的「静态内部类」方式的单例模式实现:

public class Singleton implements Serializable { private static final long serialVersionUID = -0L; private Singleton() { } private static class SingletonHolder { private static final Singleton singleton = new Singleton(); } public static synchronized Singleton getSingleton() { return SingletonHolder.singleton; } }

然后写一个验证主函数:

public class Test2 { public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream( new File("singleton.txt") ) ); // 将单例对象先序列化到文本文件singleton.txt中 objectOutputStream.writeObject( Singleton.getSingleton() ); objectOutputStream.close(); ObjectInputStream objectInputStream = new ObjectInputStream( new FileInputStream( new File("singleton.txt") ) ); // 将文本文件singleton.txt中的对象反序列化为singleton1 Singleton singleton1 = (Singleton) objectInputStream.readObject(); objectInputStream.close(); Singleton singleton2 = Singleton.getSingleton(); // 运行结果竟打印 false ! System.out.println( singleton1 == singleton2 ); } }

运行后我们发现:反序列化后的单例对象和原单例对象并不相等了,这无疑没有达到我们的目标。

解决办法是:在单例类中手写readResolve()函数,直接返回单例对象,来规避之:

private Object readResolve() { return SingletonHolder.singleton; }

这样一来,当反序列化从流中读取对象时,readResolve()会被调用,用其中返回的对象替代反序列化新建的对象。

没想到

本以为这篇会很快写完,结果又扯出了这么多东西,不过这样一梳理、一串联,感觉还是清晰了不少。

就这样吧,

来源:开源中国

作者:CodeSheep

原文:序列化/反序列化,我忍你很久了 - hansonwang的个人空间 - OSCHINA

书是人类不可或缺的精神食粮。正如高尔基所言:书是人类进步的阶梯。借名人名言给大家推荐一些对Java学习有用的经典书籍,对程序员来说,我觉得**学习方式还是看书,看视频花费时间太长,而阅读博客则不够系统。

对Java初学者最好的方式就是找到一本经典的好书,然后啃完它。当然,我还是推荐大家有时间写写博客,毕竟好记性不如烂笔头,许多的工作经验,不记录会随着时间的推移也终究会被遗忘,看书也一样,看到重点动手去实践,动脑去思考,动手记录下来,哪些书适合初学者呢?下面和千锋广州小编一起来看看吧!

1、《Head First Java》Java入门书籍

《Head First Java》是本完整的面向对象(object-oriented,OO)程序设计和Java的学习指导。你会学会如何像个面向对象开发者一样去思考。但如果你真地想要好好地学习Java,你会需要《Head First Java》强烈推荐学习此书,非常适合初学者入门。

2、《Java从入门到精通》适合自学者 这本书主要针对Java基础,对于没有学过Java的人才说,是一个不错的选择。通过这本书,大家可以从零开始,慢慢来学习,而且实操很多,不会让你看到最后出现只会理论的情况。

3、《Thinking in Java》(中文名:《Java编程思想》)适合中级自学者和培训者

是所有Java程序员必备教科书。这本书不管是正在学习还是已经工作许多年的程序员,都可以从这本书中得到你想要的东西。这本书具有教材和工具书的作用,就像一本字典,想知道什么都可以从中查询。虽然这本书很好,但并不建议初学者学习,对于初学者难度较大。

4、《疯狂Java讲义》适合自学者看 内容比较项目化,实操方法很多,如果你想进行Java的深入学习,不妨看看这本书。

5、《Java核心技术》这本书分为两个部分,第一个部分讲的是基础知识,第二个部分讲的是高级特性。但是里面对Java的技术讲述大而全,几乎对语法和基础库讲的都很夯实,我觉得入门看这个很合适。

6、《Java开发实战经典》这本书比较适合自学者学习,里面有很多小案例,可以边学边练,巩固知识。[1]

不要走马观花的学,要学会如何使用,因为学Java最开始的目的不是就是用来使用的吗?技术学了不用,那么学不学有什么区别呢

作者:艾特程序员

链接地址:https://www.sohu.com/a/_

来源:搜狐

从大学到现在,我使用Java已经将近20年,日常也带实习生,还在公司内部做training,所以可以分享下我的经验,希望对你有用。

因为是在工作中培训,就必然有两个约束:实用、时间紧,因此就不能像大学那样,把所有的知识点都面面俱到的讲到。而只能挑基础的,实用的,难理解的讲。至于其他边边角角的知识,就一笔带过。一则没有时间,二则不常用,再则既使讲了,学生印象也不深刻。总之一句话:“好钢用在刀刃上”。

下面,就根据我的实践,具体谈下学习过程:

1.基础知识

我学习java的时候,先是通读了《Java编程思想》,然后是《Java核心技术》。当时这两本书还不像现在这么厚,而刚才我把案头的《Java核心技术》第9版翻了翻,上下两册已经1700多页了,可想而知,如果要把它通读一遍,且不说把所有的代码都调通,就是当小说读,估计也需要些时间。

但我现在教学依然首推《Java核心技术》,主要是体系完整,实例多,可操作性强。但对初学者,我一般是只讲前6章,也就是下面的内容:

1,Java程序设计概述

2,Java程序设计环境

3,Java的基础程序设计结构

4,对象与

5,类继

6,承接口与内部类

就《Java核心技术》第9版来说,也就是到250页为止,加把劲,1个月拿下完全没问题。

因为你是自学,所以建议你一定要把其中的代码都调通,课后的作业尽量去做。除此之外,还有两点特别重要:

#.学习笔记

因为你是自学,不像在企业中学了就能够实践,印象自然特别深刻。而自学因为没有实践的及时反馈,所以记笔记就显得特别重要。因为记笔记就像写作一样,是整理思路的绝佳方法。同时学习笔记也是你以后开发,面试的绝好资料。

学习编程,人跟人是不一样的,别人觉得难理解的东西,对你却不一定;而你觉得难理解的东西,别人可能又会觉得特简单。而学习笔记就是自己专有的“难点手册”,有点像高考时的“错题本”,以后无论是在面试前,还是在日常工作中,随时都可以翻出来看看,自是获益匪浅。

#.分门别类保存demo

学习笔记是很好的文字资料,但编程界有句话说的特别好,所谓“no code, no text”,意思就是说:千言万语都没有一段代码来的实在。

以我的经验,在你在学习的过程中,就某个知识点,无论当时理解的多透彻,调试的多棒,只要时间一长,等到了实用的时候,肯定会碰到各种各样的问题,一些看似简单的东西,此时死活就是调不通,正所谓人到事中迷。这个时候,如果你手头恰有运行良好的demo,打开参考一下(甚至直接拷贝过来),问题自然迎刃而解。而且因为这些demo都是你亲手调试出来,印象自然特别深刻,一碰到问题,在脑子中自会立刻涌现。

所以说,在学习的过程,一定要善待你调通的demo,千万不要用完了就扔,等后来碰到困难,想要用时却找不到,追愧莫及。正确的做法就是把所有调通的demo,分门别类的保存起来,到时候查起来自是得心应手。人都说“书到用时方恨少”,其实代码也是这样,所谓“demo用时方恨少”。

2.Spring

目前在Java EE开发中,Spring已经成为和Java核心库一样的基础设施,所以说如果想成为一个合格的Java程序员,Spring肯定绕不开。另一方面,如果掌握了Spring体系,Java基本上就算入门了,就有能力进行一些实用级的开发了。

但Spring本身也是日渐复杂,衍生项目越来越多,但最最核心的概念依旧是IOC和AOP,掌握了这两个概念,再把Spring MVC学会,再学习其他的衍生项目就会平滑很多。

同时,因为Spring本身就应用了许多优雅的设计理念,所以学习Spring的过程,也是加强Java基础知识学习的过程。因此等你掌握了Spring,原来很多你理解不透彻的Java特性,此时就会恍然大悟,包括接口、抽象类等。

我学习Spring,读的第一本书是《Spring实战》,坦率的说,书很一般,但市面上比它好的书,我却没有遇到过。还有一本《Spring源码深度解析》也不错,对Spring的设计理念讲的尤其透彻,虽然整本书读起来有些艰涩,但前几章却生动有趣,也是整本书的精华。所以建议你在学习Spring之前,先把该书的前几章通读一下,然后再回过头来学习《Spring实战》会顺利很多。

以我经验,要学透Spring,终极的方法还是阅读源码(我当时就是这么干的),待把Spring的核心源码通读了,人就真的自由了(所谓无真相不自由),不仅是对Spring,而是对整个Java体系。以后再遇到其他框架,大概一眼就能看出其中的脉络,所谓到了“看山不是山”的境界。但这都是后话,可以作为以后你努力的方向。

和学习Java基础知识一样,学习Spring也一定要记笔记,一定要分门别类保存demo。

老实说,Spring对初学者不算简单,因此最好能有个好老师带一下,不用太长时间,2个课时即可,然后就是在你遇到大的困难时,能及时的点拨下。

以我的经验,要初步掌握Spring,大概需要1到1个半月的时间。

3.其他知识

Spring是Java编程的基础设施,但真要进入到实际项目的开发,还有些东西绕不过,包括 MySql,Mybatis,Redis,Servlet等,但如果你经过Spring的洗礼,这些东西相对就简单多了,以我的经验,1个月的时间足够了。

4.实践

学习Java,光学不练肯定是不行的。但因为是自学,所以就没有实际的产品让你练手,但也没有关系,谁大学还没有做过毕业设计呢?以我的经验,大家最爱的“学生管理系统”依旧是个很好的练手系统。

别看“学生管理系统”逻辑简单,但麻雀虽小五脏俱全,其中数据库设计、Mybatis,Spring、SpringMVC,Servlet、Tomcat一个都不缺,绝对的练手好伴侣。

还有,虽然你的学习重点在Java,因为要做一个完整的demo,前端的配合肯定少不了。因此就免少不了要学一些简单的JS、HTML知识,但因为前端本就是个很大的topic,所以一定要控制好边界,千万不要顾此失彼。就“学生管理系统”来说,在前端上,只要实现一个包含table、textbox、button,能发送REST请求到server,能实现学生的“增删改查”的简单页面即可。

作为一个练手项目,目标就是把Java的主要技能点串起来,所以自不求尽善尽美(也不可能),所以1个月时间足够了。.

最后

按照上面的过程,4个月的时间刚刚好。当然Java的体系是很庞大的,还有很多更高级的技能需要掌握,但不要着急,这些完全可以放到以后工作中边用别学。

学习编程就是一个由混沌到有序的过程,所以你在学习过程中,如果一时碰到理解不了的知识点,大可不必沮丧,更不要气馁,这都是正常的不能再正常的事情了,不过是“人同此心,心同此理”的暂时而已。

在日常的教学中,我常把下面这句话送给学员们,今天也把它送给你:

“道路是曲折的,前途是光明的!”祝你好运!


如果你是Java小白或者打算转行进入Java开发这个行业,不知道未来该怎么学习,都可以加入我组建的自学团,每天一起打卡学习,分享学习资料,定期组织实战项目,匹配学习伙伴,我会严格监督你们的。

作者:老鬼。。。

原文链接:零基础如何快速打好Java基础?_老鬼。。。的博客-CSDN博客

学习过Java的同学,应该都知道 Bruce Eckel 大佬的经典著作 Thinking in Java,中文版叫做《Java 编程思想》,豆瓣评分9.3,是一部已经出版十余年依旧畅销的好书。

这部作品的最新版 On Java 8 在一经发布,就广受国内读者关注,其简体中文版《On Java 中文版》将由人民邮电出版社图灵公司出版,而就在今天早上的10点24分,图灵社区刚刚上线了本书的预购链接,与以外图书出版销售不太一样的是,这次图灵社区采取了一种全新的宣发推广模式:开放出版。

  • 什么是开放出版模式?
  • 对读者而言有什么好处?
  • 《On Java 中文版》是否值得预购?

今天我就为大家深度解析一下这几个问题,当然你们关心的纸书什么时候能到手、是否有电子版、怎么购买最划算等等这些问题,我也会一一解释清楚。

什么是“开放出版”模式?

先看看图灵社区的官方定义。

先看在线电子版,再获得纸书+电子书权益,很不错的想法

简单来说,开放出版就是在纸书未上市之前(通常是翻译和写作阶段)提前线上预售,读者付费后可提前看到本书的实时内容更新。

这里可以清楚看到图书的翻译进度

以《On Java 中文版》为例,我们从图灵社区官网可以看到,目前基础卷的内容已经发布了前4章,后续的内容会保持每周更新,也就是说开放出版相当于提前阅读新书,但又不是简简单单提前阅读这么简单,它对读者而言有几点重大的意义。

“开放出版”对读者有什么好处?

首先,一本图书从写作(或翻译)、编辑、印刷到上市销售这个流程少说也要半年,作为读者大家可不想等技术凉凉了才看到图书,第一时间看到好内容、新内容,跟上技术趋势,这是技术从业者的刚需。

有人会说,并不是所有的技术书都追新,像 Java 这种成熟技术,我等纸书出版了再看也不晚嘛,为什么要购买开放出版看实时内容呢?

这是一个好问题。这里我认为出版方考虑到了另外一个重要的问题,就是阅读体验。技术图书在阅读时跟大众图书区别最大的一点就是更需要与同行交流并实践,无论是理论层面的理解还是代码实现,甚至我们经常会在阅读中去讨论作者的写作逻辑和译者的翻译技巧,大家可以看看下述几个场景你是否遇到过:

  • 这段话、这个词为什么这么翻译?
  • 这个概念用这个例子是否合适?
  • 这段代码是不是错了?为什么我跑不通?

将一本书的内容提前发布,所有参与开放出版阅读的读者都能提前阅读内容,并针对阅读中的心得、感悟、疑惑与作者、译者、编辑交流,并通过线上迭代的方式不断优化,最终保证纸质书出版的时候能够带给大家极致的阅读体验。开放出版更像是一个开源项目,每一位读者都可以成为早期贡献者。

一本好书的出版,离不开作者、译者、编辑、读者的共同努力,而在这个过程中,每一个微小的贡献都会被记录。

读者贡献墙

我在《On Java 中文版》的页面上看到了这个贡献墙,而且预购本书套装还会获得一个专属编号,可见官方对早期内容消费者的定位更趋向于贡献者的角色,希望借由开放出版这个模式构建一个内容生态圈。也许图灵社区之后会考虑给预购图书的读者增加更多的权益吧,期待一下。

说完开放出版是什么之后,我们再回到核心的问题上,这本《On Java 中文版》是否值得预购?

《On Java 中文版》预购指南

我们先看看官方放出的购买权益图:

购买权益说明

这里的几个重要信息,我给大家罗列出来:

1.预购的权益包括:纸书、电子书、在线阅读

其中纸书要等到明年3月才能上市,按照预购的顺序陆续发货;电子书要等到全部内容都编辑加工完成才能提供可下载的版本(注意是mobi格式);现在付款可以立即阅读到的就是在线阅读版本

2.纸书内容很厚,分为上下两卷

全书共1360页,分为基础卷和进阶卷,我下单的是套装,包含基础和进阶。因为套装会赠送一个定制化的封套,有收藏意义,毕竟这是Java巨著,人生需要一些仪式感。

3.限量签名是签章

这个是直接印刷在书上的,收藏价值不是很大,但貌似也只有预售版才有。

4.预售到什么时候

目前官方没有给出具体的预售截止日期和限购数量。我买的是第141套,如果是404套我会更开心。

我自己预购了259的套装,主要出于3点考虑:

1.我是个纸书控,尤其喜欢收集限量版图书;

2.我想体验下这个开放出版的模式,实测在线阅读的效果还不错,虽然没有PDF版那么痛快,但是作为早期阅读版本来讲,也是用心了。

在线阅读版的实测效果

3.早期预购者有专属读者群,这里有译者、编辑和诸多 Java 界的前辈,每天讨论的热火朝天,在这里哪怕不说话,看看都是一种收获。

《On Java 中文版》的读者交流群,挺热闹的

《On Java 中文版》怎么购买更合适

讲真259元这个价格吧,不算便宜,但也不算太贵。毕竟现在两个人吃顿饭也得2,3百的。我觉得还是看大家的具体需求而定吧:

如果你喜欢收藏纸书,又希望尽早看到内容,直接下单259的套装版本就行;

如果你是等等党,更期待电商的优惠,那也可以等3月上市后再入手,毕竟图灵社区并不经常跳票;

还是那句话,早买早享受,晚买有折扣,但不管早买晚买,大家都是正版的支持者。

希望这些信息可以帮助到你,如果《On Java 中文版》有任何最新的进展,我会随时更新这篇文章,好期待拿到限量版纸书,祝大家阅读愉快~

本文介绍了如何使用 Manifold 在 Java 中实现操作符重载,来助力提升开发效率和代码可读性。

作者 | 周密(之叶)

来源 | 阿里开发者公众号

什么是操作符重载

操作符重载,就是把已经定义的、有一定功能的操作符进行重新定义,来完成更为细致具体的运算等功能。从面向对象的角度说,就是可以将操作符定义为类的方法,使得该操作符的功能可以用来代表对象的某个行为。

为什么需要操作符重载

我们来考虑实现这样的功能:使用 BigInteger 来实现的完全平方差公式($a^2 - 2ab + b^2$)

private static final BigInteger BI_2 = BigInteger.valueOf(2);

常规写法:

BigInteger res = a.multiply(a).subtract(BI_2.multiply(a).multiply(b)).add(b.multiply(b));

假设可以对 Java 中的 *+- 进行操作符重载,那么我们就可以直接这样写:

BigInteger res = a * a - BI_2 * a * b + b * b;

所以,对于非原始类型的数值运算,如果能够进行操作符重载,至少有 2 个好处:

  1. 代码写起来更简单,而且不容易出错
  2. 代码更容易阅读,不会一堆括号套来套去

如何在 Java 中实现操作符重载

在 Java 中实现操作符重载,依然是使用我们的黑科技 Manifold。Manifold 可以为 Java 提供各种场景操作符的重载功能,例如算数操作符(包括 +-*/%)、比较操作符(>>=<<===!=)、索引操作符(即 [])等。关于 Manifold 的集成,可以参考上一篇文章:Java 缺失的特性:扩展方法

算数操作符

Manifold 是将每个算数操作符的重载,映射到特定名称的函数。例如你在某个类 A 中定义了 plus(B) 的方法,那么这个类就可以使用 a + b 代替 a.plus(b) 进行调用。具体的映射关系为:

操作符 方法调用
c = a + b c = a.plus(b)
c = a - b c = a.minus(b)
c = a * b c = a.times(b)
c = a / b c = a.div(b)
c = a % b c = a.rem(b)

—— 用过 Kotlin 的同学应该会会心一笑,这就是模仿的 Kotlin 的操作符重载。

为了方便举例说明,我们定义一个数值类型 Num

public class Num { private final int v; public Num(int v) { this.v = v; } public Num plus(Num that) { return new Num(this.v + that.v); } public Num minus(Num that) { return new Num(this.v - that.v); } public Num times(Num that) { return new Num(this.v * that.v); } }

对于下面的代码:

Num a = new Num(1); Num b = new Num(2); Num c = a + b - a;

Manifold 在编译期处理之后,会变成:

在数学运算上操作符存在优先级,Manifold 当然也是支持的。所以对于这样的代码:

Num c = a + a * b - b;

Manifold 处理之后,则是:

而且因为 Java 支持方法重载,所以对于 plus 方法,可以接收多种类型的参数。

public class Num { ... public Num plus(Num that) { return new Num(this.v + that.v); } public Num plus(int i) { return new Num(v + i); } }

这极大的增强了操作符重载的能力:

Num c = a + 1 + b;

在 Manifold 处理之后:

值得注意的是,因为 +* 都是满足交换律的,所以 a + b 首先会去对象 a 中寻找符合的 plus 方法,如果 a 中存在,则执行的是 a.plus(b);如果 a 中不存在,而 b 中存在符合的 plus 方法,则执行的是 b.plus(a)a * b 同理。

Java 对原始类型中的数值支持复合赋值,即 +=-= 这些,Manifold 也支持:

操作符 方法调用
a += b a = a.plus(b)
a -= b a = a.minus(b)
a *= b a = a.times(b)
a /= b a = a.div(b)
a %= b a = a.rem(b)

如果是现有的库,不能直接给它的类加这些方法该怎么办?别忘了 Manifold 支持扩展方法的哦。

比较操作符

我们都知道,对于非原始类型的 Java 对象,进行大小的比较用的是 Comparable<T>。如果你的对象实现了 Comparable<T>,那么恭喜你,Manifold 直接让你拥有了 >>=<<= 这四个比较操作符的重载:

操作符 方法调用
a > b a.compareTo(b) > 0
a >= b a.compareTo(b) >= 0
a < b a.compareTo(b) < 0
a <= b a.compareTo(b) <= 0

我们让 Num 实现 Comparable<Num>

public class Num implements Comparable<Num> { ... @Override public int compareTo(Num that) { return this.v - that.v; } }

那么对于这样的代码:

Num a = new Num(1); Num b = new Num(2); if (a > b) { System.out.println("a > b"); } if (a < b) { System.out.println("a < b"); }

运行代码会输出 a < b,因为代码在被 Manifold 处理之后会变为:

你是不是激动的要问,那么==!=呢,Manifold 支持了吗?是的,我的朋友,它支持了(翻译腔)。Manifold 提供了一个新的接口ComparableUsing<T>,通过它你可以实现对==!=的重载。

ComparableUsing<T> 继承了 Comparable<T> 接口,并且添加了两个方法,compareToUsingequalityMode。查看 comparableUsing 的默认实现:

可见对于>>=<<=这四种操作符的重载,直接是使用Comparable<T>compareTo的实现。而对于==!=,则是根据equalityMode方法的返回值,来选择使用何种实现:

  • 如果是 EqualityMode.CompareTo,则 ==!= 的重载分别对应的是 compareTo 方法返回值为 0 和 非0 的情况
  • 如果是 EqualityMode.Equals,则 ==!= 的重载分别对应的是 equals 方法返回值为 truefalse 的情况
  • 如果是 EqualityMode.Identity,那使用的是 Java 的默认实现,即比较对象的引用地址是否相同

equalityMode 默认的方法返回值为 EqualityMode.Equals,即 Manifold 默认使用 equals 方法来进行 ==!= 的判断。当然,你也可以不使用 Manifold 的 equalityMode 这套逻辑,直接实现自己的 compareUsing 方法,处理各种 Operator 的比较逻辑。

我们让 Num 实现 ComparableUsing<Num> 接口,并覆写 equals

public class Num implements ComparableUsing<Num> { ... @Override public int compareTo(Num that) { return this.v - that.v; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof Num) { Num that = (Num) obj; return this.v == that.v; } return false; } @Override public int hashCode() { return Objects.hash(v); } }

则此时我们对 ==!= 进行了重载,并且使用的是基于 equals 方法的实现。那么对于下面的代码:

Num a = new Num(1); Num b = new Num(1); if (a == b) { System.out.println("a == b"); } if (a != b) { System.out.println("a != b"); }

运行代码会打印 a == b,因为 Manifold 处理之后的代码会变为:

Amazing!我们终于实现了 N 年前的梦想,让 ==!= 是使用 equals 方法的逻辑进行比较,而不是比较引用地址。

你应该也发现了,如果某个类型 T 要实现 ComparableUsing<T>,那么说明 T 一定是 Comparable<T>。也就是说,如果你想要对 T 重载 ==!=,则要求 T 一定是可比较的。Manifold 之所以这样做,而不是为重载 ==!= 提供单独的接口,是因为作者目前认为用 ==!= 来代替 equals,弊大于利 —— 毕竟用 equals 来比较两个对象是否相等这件事,在 Java 中太深入人心了。所以目前 Manifold 作者希望大家只对数值和量词这类的对象使用 ==!=,不要产生滥用行为。

如果是现有的库,比如 StringBigInteger,不能直接给它的类新增接口实现怎么办?你可以给这个类建一个扩展类,然后让扩展类实现 ComparableUsing<T>,然后 Manifold 会按照这个类实现了 ComparableUsing<T> 进行处理。比如 Manifold 对于 BigInteger 的扩展类 ManBigIntegerExt(位于 manifold-science 库中):

它以扩展方法的形式,提供了自定义逻辑的compareUsing实现:

注意,这个时候要用abstract关键字修饰扩展类,因为它不是真的要以常规方式来实现ComparableUsing<T>接口。或者,你也可以把扩展类声明为接口,然后继承ComparableUsing<T>接口。

索引操作符

Java 对数组是支持索引操作符的,比如 nums[i] 是访问数组索引为 i 的元素,nums[i] = n 是对数组索引为 i 的位置进行赋值。但对 ListMap,Java 说 “不好意思,因为我是 Java,这个支持不了”。所以 Manifold 又出手了,让你不再只能羡慕其他语言。

操作符 方法调用
c = a[b] c = a.get(b)
a[b] = c a.set(b, c)

因为 java.util.List 已经具备了这两个方法,所以有了 Manifold,你可以这样写代码:

Map 只有 get 方法,没有 set 方法,所以你可以在 Map 扩展类里面,加一个 set

@Extension public class MapExt { public static <K, V> V set(@This Map<K, V> map, K key, V value) { return map.put(key, value); } }

然后我们就可以这样写代码了:

简直不要太爽!需要注意的是,Manifold 对 set 方法是有要求的:set 方法的返回值不能为 void,并且应该返回和第二个参数一样类型的值(一般是返回旧值)。之所以有这样的要求,是为了和 Java 本身的数组的索引赋值表达式保持一致(如果 set 返回的是 void,索引赋值表达式就无法支持了)。在 Java 中,你可以这样赋值:

int[] nums = {1, 2, 3}; int value = nums[0] = 10;

执行完成之后,num[0]value,都会是 10。所以,当我们使用索引赋值表达式的时候:

List<String> list = Arrays.asList("a", "b", "c"); String value = list[0] = "A";

Manifold 处理之后,代码会变成:

因而类似于T value = list[0] = obj的表达式,执行完之后value不是set方法的返回值,而是最右侧的值。

单位操作符

Manifold 还提供一种非常有意思的功能:单位操作符。顾名思义,就是我们可以在代码中提供“单位”功能。比如下面这种代码:

你是不是已经惊呆了?我第一次见到的时候也是满脑子“还能这样操作”的惊奇。而这个dt,就是“单位”。查看 Manifold 处理后的代码:

也就是说 Manifold 将 "xxx"dt 替换为了 dt.postfixBind("xxx"),那么你也就可以猜到 DateTimeUnit 类的代码:

public class DateTimeUnit { private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); public LocalDateTime postfixBind(String value) { return LocalDateTime.parse(value, FORMATTER); } }

postfixBind 表示这个单位是“后缀单位”,就是你看到的 "xxx"dtdt 在 "xxx" 的后面。Manifold 同时也支持“前缀单位”,对应的方法是 prefixBind,比如:

public class DateTimeUnit { ... public LocalDateTime prefixBind(String value) { return LocalDateTime.parse(value, FORMATTER); } }

添加了 prefixBind(String) 后,那么就可以这样定义 LocalDateTime

Amazing!有了“单位”功能,我们就可以做出很多实用的“字面量”功能。比如定义 BigInteger 的“单位”:

public class BigIntegerUnit { public BigInteger postfixBind(Integer value) { return BigInteger.valueOf(value); } public BigInteger postfixBind(String value) { return new BigInteger(value); } }

配合 Manifold 的 auto(类似于 Java10 提供的 var,但是 auto 还可以用来定义属性):

谁还会认为你用的是 Java8?对于不知道 Manifold 的同事,你和他说你用的是一门新的名叫 Java888 的语言,他都会相信的 :)。而且我们还可以将 postfixBindprefixBind 放在一起使用,比如提供下面的类:

public class MapEntryBuilder { public <K> EntryKey<K> postfixBind(K key) { return new EntryKey<>(key); } public static class EntryKey<K> { private final K key; public EntryKey(K key) { this.key = key; } public <V> Map.Entry<K, V> prefixBind(V value) { return new AbstractMap.SimpleImmutableEntry<>(key, value); } } }

那么,便可以通过这种方式来创建 Map.Entry

先通过 to.postfixBind 创建 EntryKey,再通过 EntryKeyprefixBind 方法创建 Map.Entry。如果我们再为 Map 提供如下静态扩展方法:

@Extension public class MapExt { @Extension @SafeVarargs public static <K, V> Map<K, V> of(Map.Entry<K, V>... entries) { Map<K, V> map = new LinkedHashMap<>(entries.length); for (Map.Entry<K, V> entry : entries) { map.put(entry.getKey(), entry.getValue()); } return Collections.unmodifiableMap(map); } }

那么你可以这样创建 Map

建议

Java 一直以来都不支持操作符重载,肯定是有其原因的。作为一门之前主打企业应用开发的语言,确实操作符重载不是必要的。但随着硬件的发展,我们也看到 Java 越来越多的出现在数据科学/高性能计算的领域,同时 Java 也开始尝试提供值类型:Project Valhalla。所以,也许在不久后的将来,随着值类型的应用,在 Java 中提供操作符重载的呼声会越来越高,进而被 JCP 采纳。而 Manifold 作为先驱者,提前让我们可以体验未来的 Java,幸甚至哉!

当然,和扩展方法一样,如果决定在项目中采用 Manifold 提供操作符重载,我们一定要做到“管住自己的手”。当想要添加某个操作符重载时,一定要先问自己一遍 “这个类是否具备该操作符对应语义的功能,用操作符写的代码是否会降低代码可读性”。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

版权声明


相关文章:

  • java语言基础表达式2024-11-02 23:50:01
  • java数组基础头歌2024-11-02 23:50:01
  • java 8基础面试2024-11-02 23:50:01
  • java程序设计基础事件处理2024-11-02 23:50:01
  • java学完基础之后2024-11-02 23:50:01
  • java基础案例教程怎么样2024-11-02 23:50:01
  • java 基础类型数组合并2024-11-02 23:50:01
  • sap需要有java基础吗2024-11-02 23:50:01
  • 没有英文基础可以自学java吗2024-11-02 23:50:01
  • java 序列化基础2024-11-02 23:50:01