[{"content":" [TOC]\njava匠人手法-优雅的处理空值 # 导语 # 在笔者几年的开发经验中，经常看到项目中存在到处空值判断的情况，这些判断，会让人觉得摸不这头绪，它的出现很有可能和当前的业务逻辑并没有关系。但它会让你很头疼。 有时候，更可怕的是系统因为这些空值的情况，会抛出空指针异常，导致业务系统发生问题。 此篇文章，我总结了几种关于空值的处理手法，希望对读者有帮助。\n业务中的空值 # 场景 # 存在一个UserSearchService用来提供用户查询的功能:\npublic interface UserSearchService{ List\u0026lt;User\u0026gt; listUser(); User get(Integer id); } 问题现场 # 对于面向对象语言来讲，抽象层级特别的重要。尤其是对接口的抽象，它在设计和开发中占很大的比重，我们在开发时希望尽量面向接口编程。 对于以上描述的接口方法来看，大概可以推断出可能它包含了以下两个含义:\nlistUser(): 查询用户列表 get(Integer id): 查询单个用户 在所有的开发中，XP推崇的TDD模式可以很好的引导我们对接口的定义，所以我们将TDD作为开发代码的”推动者”。 对于以上的接口，当我们使用TDD进行测试用例先行时，发现了潜在的问题：\nlistUser() 如果没有数据，那它是返回空集合还是null呢？ get(Integer id) 如果没有这个对象，是抛异常还是返回null呢？ 深入listUser研究 # 我们先来讨论\nlistUser() 这个接口，我经常看到如下实现:\npublic List\u0026lt;User\u0026gt; listUser(){ List\u0026lt;User\u0026gt; userList = userListRepostity.selectByExample(new UserExample()); if(CollectionUtils.isEmpty(userList)){//spring util工具类 return null; } return userList; } 这段代码返回是null,从我多年的开发经验来讲，对于集合这样返回值，最好不要返回null，因为如果返回了null，会给调用者带来很多麻烦。你将会把这种调用风险交给调用者来控制。 如果调用者是一个谨慎的人，他会进行是否为null的条件判断。如果他并非谨慎，或者他是一个面向接口编程的狂热分子(当然，面向接口编程是正确的方向)，他会按照自己的理解去调用接口，而不进行是否为null的条件判断，如果这样的话，是非常危险的，它很有可能出现空指针异常！ 根据墨菲定律来判断: “很有可能出现的问题，在将来一定会出现!”\n基于此，我们将它进行优化:\npublic List\u0026lt;User\u0026gt; listUser(){ List\u0026lt;User\u0026gt; userList = userListRepostity.selectByExample(new UserExample()); if(CollectionUtils.isEmpty(userList)){ return Lists.newArrayList();//guava类库提供的方式 } return userList; } 对于接口(List listUser())，它一定会返回List，即使没有数据，它仍然会返回List（集合中没有任何元素）; 通过以上的修改，我们成功的避免了有可能发生的空指针异常，这样的写法更安全！\n深入研究get方法 # 对于接口\nUser get(Integer id) 你能看到的现象是，我给出id，它一定会给我返回User.但事实真的很有可能不是这样的。\n我看到过的实现:\npublic User get(Integer id){ return userRepository.selectByPrimaryKey(id);//从数据库中通过id直接获取实体对象 } 相信很多人也都会这样写。 通过代码的时候得知它的返回值很有可能是null! 但我们通过的接口是分辨不出来的! 这个是个非常危险的事情。尤其对于调用者来说！\n我给出的建议是，需要在接口明明时补充文档,比如对于异常的说明,使用注解@exception:\npublic interface UserSearchService{ /** * 根据用户id获取用户信息 * @param id 用户id * @return 用户实体 * @exception UserNotFoundException */ User get(Integer id); } 我们把接口定义加上了说明之后，调用者会看到，如果调用此接口，很有可能抛出“UserNotFoundException(找不到用户)”这样的异常。\n这种方式可以在调用者调用接口的时候看到接口的定义，但是，这种方式是”弱提示”的！ 如果调用者忽略了注释，有可能就对业务系统产生了风险，这个风险有可能导致一个亿！\n除了以上这种”弱提示”的方式，还有一种方式是，返回值是有可能为空的。那要怎么办呢？ 我认为我们需要增加一个接口，用来描述这种场景. 引入jdk8的Optional,或者使用guava的Optional.看如下定义:\npublic interface UserSearchService{ /** * 根据用户id获取用户信息 * @param id 用户id * @return 用户实体,此实体有可能是缺省值 */ Optional\u0026lt;User\u0026gt; getOptional(Integer id); } Optional有两个含义: 存在 or 缺省。\n那么通过阅读接口getOptional()，我们可以很快的了解返回值的意图，这个其实是我们想看到的，它去除了二义性。\n它的实现可以写成:\npublic Optional\u0026lt;User\u0026gt; getOptional(Integer id){ return Optional.ofNullable(userRepository.selectByPrimaryKey(id));} 深入入参 # 通过上述的所有接口的描述，你能确定入参id一定是必传的吗？ 我觉得答案应该是：不能确定。除非接口的文档注释上加以说明。\n那如何约束入参呢?\n我给大家推荐两种方式：\n强制约束 文档性约束（弱提示） 1.强制约束，我们可以通过jsr 303进行严格的约束声明:\npublic interface UserSearchService{ /** * 根据用户id获取用户信息 * @param id 用户id * @return 用户实体 * @exception UserNotFoundException */ User get(@NotNull Integer id); /** * 根据用户id获取用户信息 * @param id 用户id * @return 用户实体,此实体有可能是缺省值 */ Optional\u0026lt;User\u0026gt; getOptional(@NotNull Integer id);} 当然，这样写，要配合AOP的操作进行验证，但让spring已经提供了很好的集成方案，在此我就不在赘述了。\n2.文档性约束\n在很多时候，我们会遇到遗留代码，对于遗留代码，整体性改造的可能性很小。 我们更希望通过阅读接口的实现，来进行接口的说明。 jsr 305规范，给了我们一个描述接口入参的一个方式(需要引入库 com.google.code.findbugs:jsr305):\n可以使用注解: @Nullable @Nonnull @CheckForNull 进行接口说明。 比如:\npublic interface UserSearchService{ /** * 根据用户id获取用户信息 * @param id 用户id * @return 用户实体 * @exception UserNotFoundException */ @CheckForNull User get(@NonNull Integer id); /** * 根据用户id获取用户信息 * @param id 用户id * @return 用户实体,此实体有可能是缺省值 */ Optional\u0026lt;User\u0026gt; getOptional(@NonNull Integer id);} 小结 # 通过 空集合返回值,Optional,jsr 303，jsr 305这几种方式，可以让我们的代码可读性更强，出错率更低！\n空集合返回值 ： 如果有集合这样返回值时，除非真的有说服自己的理由，否则，一定要返回空集合，而不是null Optional: 如果你的代码是jdk8，就引入它！ 如果不是，则使用Guava的Optional,或者升级jdk版本！ 它很大程度的能增加了接口的可读性！ jsr 303: 如果新的项目正在开发，不防加上这个试试！ 一定有一种特别爽的感觉! jsr 305: 如果老的项目在你的手上，你可以尝试的加上这种文档型注解，有助于你后期的重构，或者新功能增加了，对于老接口的理解! 空对象模式 # 场景 # 我们来看一个DTO转化的场景，对象:\n@Datastatic class PersonDTO{ private String dtoName; private String dtoAge;}@Datastatic class Person{ private String name; private String age;} 需求是将Person对象转化成PersonDTO，然后进行返回。 当然对于实际操作来讲，返回如果Person为空，将返回null,但是PersonDTO是不能返回null的（尤其Rest接口返回的这种DTO）。 在这里，我们只关注转化操作，看如下代码:\n@Testpublic void shouldConvertDTO(){ PersonDTO personDTO = new PersonDTO(); Person person = new Person(); if(!Objects.isNull(person)){ personDTO.setDtoAge(person.getAge()); personDTO.setDtoName(person.getName()); }else{ personDTO.setDtoAge(\u0026#34;\u0026#34;); personDTO.setDtoName(\u0026#34;\u0026#34;); }} 优化修改 # 这样的数据转化，我们认识可读性非常差，每个字段的判断，如果是空就设置为空字符串(“”)\n换一种思维方式进行思考，我们是拿到Person这个类的数据，然后进行赋值操作(setXXX),其实是不关系Person的具体实现是谁的。\n那我们可以创建一个Person子类:\nstatic class NullPerson extends Person{ @Override public String getAge() { return \u0026#34;\u0026#34;; } @Override public String getName() { return \u0026#34;\u0026#34;; }} 它作为Person的一种特例而存在，如果当Person为空的时候，则返回一些get*的默认行为.\n所以代码可以修改为:\n@Test public void shouldConvertDTO(){ PersonDTO personDTO = new PersonDTO(); Person person = getPerson(); personDTO.setDtoAge(person.getAge()); personDTO.setDtoName(person.getName()); } private Person getPerson(){ return new NullPerson();//如果Person是null ,则返回空对象 } 其中getPerson()方法，可以用来根据业务逻辑获取Person有可能的对象（对当前例子来讲，如果Person不存在，返回Person的的特例NUllPerson），如果修改成这样，代码的可读性就会变的很强了。\n使用Optional可以进行优化 # 空对象模式，它的弊端在于需要创建一个特例对象，但是如果特例的情况比较多，我们是不是需要创建多个特例对象呢，虽然我们也使用了面向对象的多态特性，但是，业务的复杂性如果真的让我们创建多个特例对象，我们还是要再三考虑一下这种模式，它可能会带来代码的复杂性。\n对于上述代码，还可以使用Optional进行优化。\n@Test public void shouldConvertDTO(){ PersonDTO personDTO = new PersonDTO(); Optional.ofNullable(getPerson()).ifPresent(person -\u0026gt; { personDTO.setDtoAge(person.getAge()); personDTO.setDtoName(person.getName()); }); } private Person getPerson(){ return null; } Optional对空值的使用，我觉得更为贴切，它只适用于”是否存在”的场景。 如果只对控制的存在判断，我建议使用Optional.\nOptioanl的正确使用 # Optional如此强大，它表达了计算机最原始的特性(0 or 1),那它如何正确的被使用呢!\nOptional不要作为参数 # 如果你写了一个public方法，这个方法规定了一些输入参数，这些参数中有一些是可以传入null的，那这时候是否可以使用Optional呢？\n我给的建议是: 一定不要这样使用!\n举个例子:\npublic interface UserService{ List\u0026lt;User\u0026gt; listUser(Optional\u0026lt;String\u0026gt; username);} 这个例子的方法 listUser,可能在告诉我们需要根据username查询所有数据集合，如果username是空，也要返回所有的用户集合.\n当我们看到这个方法的时候，会觉得有一些歧义:\n“如果username是absent,是返回空集合吗？还是返回全部的用户数据集合？”\nOptioanl是一种分支的判断，那我们究竟是关注 Optional还是Optional.get()呢？\n我给大家的建议是，如果不想要这样的歧义，就不要使用它！\n如果你真的想表达两个含义，就給它拆分出两个接口:\npublic interface UserService{ List\u0026lt;User\u0026gt; listUser(String username); List\u0026lt;User\u0026gt; listUser();} 我觉得这样的语义更强，并且更能满足 软件设计原则中的 “单一职责”。\n如果你觉得你的入参真的有必要可能传null,那请使用jsr 303或者jsr 305进行说明和验证!\n请记住! Optional不能作为入参的参数!\nOptional作为返回值 # 当个实体的返回 # 那Optioanl可以做为返回值吗？ 其实它是非常满足是否存在这个语义的。\n你如说，你要根据id获取用户信息，这个用户有可能存在或者不存在。\n你可以这样使用:\npublic interface UserService{ Optional\u0026lt;User\u0026gt; get(Integer id);} 当调用这个方法的时候，调用者很清楚get方法返回的数据，有可能不存在，这样可以做一些更合理的判断，更好的防止空指针的错误！\n当然，如果业务方真的需要根据id必须查询出User的话，就不要这样使用了，请说明，你要抛出的异常.\n只有当考虑它返回null是合理的情况下，才进行Optional的返回\n集合实体的返回 # 不是所有的返回值都可以这样用的！ 如果你返回的是集合：\npublic interface UserService{ Optional\u0026lt;List\u0026lt;User\u0026gt;\u0026gt; listUser();} 这样的返回结果，会让调用者不知所措，是否我判断Optional之后，还用进行isEmpty的判断呢？\n这样带来的返回值歧义！ 我认为是没有必要的。\n我们要约定，对于List这种集合返回值，如果集合真的是null的，请返回空集合(Lists.newArrayList);\n使用Optional变量 # Optional\u0026lt;User\u0026gt; userOpt = ... 如果有这样的变量userOpt,请记住 ：\n一定不能直接使用get ，如果这样用，就丧失了Optional本身的含义 （ 比如userOp.get() ） 不要直接使用getOrThrow ,如果你有这样的需求：获取不到就抛异常。 那就要考虑，是否是调用的接口设计的是否合理 getter中的使用 # 对于一个java bean,所有的属性都有可能返回null,那是否需要改写所有的getter成为Optional类型呢？\n我给大家的建议是，不要这样滥用Optional.\n即便 我java bean中的getter是符合Optional的，但是因为java bean 太多了，这样会导致你的代码有50%以上进行Optinal的判断，这样便污染了代码。(我想说，其实你的实体中的字段应该都是由业务含义的，会认真的思考过它存在的价值的，不能因为Optional的存在而滥用)\n我们应该更关注于业务，而不只是空值的判断。\n请不要在getter中滥用Optional.\n小结 # 可以这样总结Optional的使用：\n当使用值为空的情况，并非源于错误时，可以使用Optional! Optional不要用于集合操作! 不要滥用Optional,比如在java bean的getter中! ","date":"2021-07-23","externalUrl":null,"permalink":"/2021/java-handle-null/","section":"Posts","summary":"Java 最佳实践","title":"java匠人手法-优雅的处理空值","type":"posts"},{"content":" [TOC]\n细思极恐-你真的会写java吗? # 导语 # 自2013年毕业后，今年已经是我工作的第4个年头了，总在做java相关的工作，终于有时间坐下来，写一篇关于java写法的一篇文章，来探讨一下如果你真的是一个java程序员，那你真的会写java吗? 笔者是一个务实的程序员，故本文绝非扯淡文章，文中内容都是干货，望读者看后，能有所收获。\n文章核心 # 其实，本不想把标题写的那么恐怖，只是发现很多人干了几年java以后，都自认为是一个不错的java程序员了，可以拿着上万的工资都处宣扬自己了，写这篇文章的目的并不是嘲讽和我一样做java的同行们，只是希望读者看到此骗文章后，可以和我一样，心平气和的争取做一个优秀的程序员。\n讲述方向 # 由于一直从事移动互联网相关工作，java开发中经常和移动端打交道或者做一些后端的工作，所以本篇文章更可能涉及和移动端的交互或者与后端的交互方式，笔者希望以自身的一些学习经验或者开发经验，可以带动认真阅读本篇文章的读者们，让大家对java有一个更好的态度去学习它，它不只是一个赚钱的工具而已。\n笔者身边有很多与笔者年龄相仿或年龄更大的朋友或同事，经常有人问我：“你现在还在学习吗？我觉得没什么好学的，这些东西都差不多”，我总是回答只要有时间，我就要看一会书，这个时候，大家都会露出一副不屑的眼神或笑容。其实，非常能理解身边朋友或同事的看法，以目前状态来讲，大多都是工作至少5年的程序员了，对于公司大大小小的业务需要，以目前的知识储备来讲，都可以轻松应对，“没有什么好学的”其实这句话没有多大的问题，但是，如果你对编程还有一点点兴趣，只是不知道如何努力或改进，希望本篇文章可以帮到你。\n技术点 # 本文不是一个吹嘘的文章，不会讲很多高深的架构，相反，会讲解很多基础的问题和写法问题，如果读者自认为基础问题和写法问题都是不是问题，那请忽略这篇文章，节省出时间去做一些有意义的事情。\n开发工具 # 不知道有多少”老”程序员还在使用eclipse，这些程序员们要不就是因循守旧，要不就是根本就不知道其他好的开发工具的存在，eclipse吃内存卡顿的现象以及各种偶然莫名异常的出现，都告知我们是时候寻找新的开发工具了。\n更换IDE # 根本就不想多解释要换什么样的IDE，如果你想成为一个优秀的java程序员，请更换intellij idea. 使用idea的好处，请搜索谷歌。\n别告诉我快捷键不好用 # 更换IDE不在我本文的重点内容中，所以不下想用太多的篇幅去写为什么更换IDE，请谷歌。\n在这里，我只能告诉你，更换IDE只为了更好、更快的写好java代码。原因略。\n别告诉我快捷键不好用，请尝试新事物。\nbean # bean使我们使用最多的模型之一，我将以大篇幅去讲解bean，希望读者好好体会。\ndomain包名 # 根据很多java程序员的”经验”来看，一个数据库表则对应着一个domain对象，所以很多程序员在写代码时，包名则使用：com.xxx.domain ，这样写好像已经成为了行业的一种约束，数据库映射对象就应该是domain。但是你错了，domain是一个领域对象，往往我们再做传统java软件web开发中，这些domain都是贫血模型，是没有行为的，或是没有足够的领域模型的行为的，所以，以这个理论来讲，这些domain都应该是一个普通的entity对象，并非领域对象，所以请把包名改为:com.xxx.entity。\n如果你还不理解我说的话，请看一下Vaughn Vernon出的一本叫做《IMPLEMENTING DOMAIN-DRIVEN DESIGN》(实现领域驱动设计)这本书，书中讲解了贫血模型与领域模型的区别，相信你会受益匪浅。\nDTO # 数据传输我们应该使用DTO对象作为传输对象，这是我们所约定的，因为很长时间我一直都在做移动端api设计的工作，有很多人告诉我，他们认为只有给手机端传输数据的时候(input or output)，这些对象成为DTO对象。请注意！这种理解是错误的，只要是用于网络传输的对象，我们都认为他们可以当做是DTO对象，比如电商平台中，用户进行下单，下单后的数据，订单会发到OMS 或者 ERP系统，这些对接的返回值以及入参也叫DTO对象。\n我们约定某对象如果是DTO对象，就将名称改为XXDTO,比如订单下发OMS：OMSOrderInputDTO。\nDTO转化 # 正如我们所知，DTO为系统与外界交互的模型对象，那么肯定会有一个步骤是将DTO对象转化为BO对象或者是普通的entity对象，让service层去处理。\n场景 # 比如添加会员操作，由于用于演示，我只考虑用户的一些简单数据，当后台管理员点击添加用户时，只需要传过来用户的姓名和年龄就可以了，后端接受到数据后，将添加创建时间和更新时间和默认密码三个字段，然后保存数据库。\n@RequestMapping(\u0026#34;/v1/api/user\u0026#34;) @RestController public class UserApi { @Autowired private UserService userService; @PostMapping public User addUser(UserInputDTO userInputDTO){ User user = new User(); user.setUsername(userInputDTO.getUsername()); user.setAge(userInputDTO.getAge()); return userService.addUser(user); } } 我们只关注一下上述代码中的转化代码，其他内容请忽略:\nUser user = new User(); user.setUsername(userInputDTO.getUsername()); user.setAge(userInputDTO.getAge()); 请使用工具 # 上边的代码，从逻辑上讲，是没有问题的，只是这种写法让我很厌烦，例子中只有两个字段，如果有20个字段，我们要如何做呢？ 一个一个进行set数据吗？当然，如果你这么做了，肯定不会有什么问题，但是，这肯定不是一个最优的做法。\n网上有很多工具，支持浅拷贝或深拷贝的Utils. 举个例子，我们可以使用org.springframework.beans.BeanUtils#copyProperties对代码进行重构和优化:\n@PostMapping public User addUser(UserInputDTO userInputDTO){ User user = new User(); BeanUtils.copyProperties(userInputDTO,user); return userService.addUser(user); } BeanUtils.copyProperties是一个浅拷贝方法，复制属性时，我们只需要把DTO对象和要转化的对象两个的属性值设置为一样的名称，并且保证一样的类型就可以了。如果你在做DTO转化的时候一直使用set进行属性赋值，那么请尝试这种方式简化代码，让代码更加清晰!\n转化的语义 # 上边的转化过程，读者看后肯定觉得优雅很多，但是我们再写java代码时，更多的需要考虑语义的操作，再看上边的代码:\nUser user = new User(); BeanUtils.copyProperties(userInputDTO,user); 虽然这段代码很好的简化和优化了代码，但是他的语义是有问题的，我们需要提现一个转化过程才好,所以代码改成如下:\n@PostMapping public User addUser(UserInputDTO userInputDTO){ User user = convertFor(userInputDTO); return userService.addUser(user); } private User convertFor(UserInputDTO userInputDTO){ User user = new User(); BeanUtils.copyProperties(userInputDTO,user); return user; } 这是一个更好的语义写法，虽然他麻烦了些，但是可读性大大增加了，在写代码时，我们应该尽量把语义层次差不多的放到一个方法中，比如:\nUser user = convertFor(userInputDTO); return userService.addUser(user); 这两段代码都没有暴露实现，都是在讲如何在同一个方法中，做一组相同层次的语义操作，而不是暴露具体的实现。\n如上所述，是一种重构方式，读者可以参考Martin Fowler的《Refactoring Imporving the Design of Existing Code》(重构 改善既有代码的设计) 这本书中的Extract Method重构方式。\n抽象接口定义 # 当实际工作中，完成了几个api的DTO转化时，我们会发现，这样的操作有很多很多，那么应该定义好一个接口，让所有这样的操作都有规则的进行。 如果接口被定义以后，那么convertFor这个方法的语义将产生变化，他将是一个实现类。\n看一下抽象后的接口:\npublic interface DTOConvert\u0026lt;S,T\u0026gt; { T convert(S s); } 虽然这个接口很简单，但是这里告诉我们一个事情，要去使用泛型，如果你是一个优秀的java程序员，请为你想做的抽象接口，做好泛型吧。\n我们再来看接口实现: public class UserInputDTOConvert implements DTOConvert { @Override public User convert(UserInputDTO userInputDTO) { User user = new User(); BeanUtils.copyProperties(userInputDTO,user); return user; } }\n我们这样重构后，我们发现现在的代码是如此的简洁，并且那么的规范:\n@RequestMapping(\u0026#34;/v1/api/user\u0026#34;) @RestController public class UserApi { @Autowired private UserService userService; @PostMapping public User addUser(UserInputDTO userInputDTO){ User user = new UserInputDTOConvert().convert(userInputDTO); return userService.addUser(user); } } review code # 如果你是一个优秀的java程序员，我相信你应该和我一样，已经数次重复review过自己的代码很多次了。 我们再看这个保存用户的例子，你将发现，api中返回值是有些问题的，问题就在于不应该直接返回User实体，因为如果这样的话，就暴露了太多实体相关的信息，这样的返回值是不安全的，所以我们更应该返回一个DTO对象，我们可称它为UserOutputDTO:\n@PostMapping public UserOutputDTO addUser(UserInputDTO userInputDTO){ User user = new UserInputDTOConvert().convert(userInputDTO); User saveUserResult = userService.addUser(user); UserOutputDTO result = new UserOutDTOConvert().convertToUser(saveUserResult); return result; } 这样你的api才更健全。\n不知道在看完这段代码之后，读者有是否发现还有其他问题的存在，作为一个优秀的java程序员，请看一下这段我们刚刚抽象完的代码: User user = new UserInputDTOConvert().convert(userInputDTO); 你会发现，new这样一个DTO转化对象是没有必要的，而且每一个转化对象都是由在遇到DTO转化的时候才会出现，那我们应该考虑一下，是否可以将这个类和DTO进行聚合呢，看一下我的聚合结果: public class UserInputDTO { private String username; private int age;\npublic String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public User convertToUser(){ UserInputDTOConvert userInputDTOConvert = new UserInputDTOConvert(); User convert = userInputDTOConvert.convert(this); return convert; } private static class UserInputDTOConvert implements DTOConvert\u0026lt;UserInputDTO,User\u0026gt; { @Override public User convert(UserInputDTO userInputDTO) { User user = new User(); BeanUtils.copyProperties(userInputDTO,user); return user; } } } 然后api中的转化则由: User user = new UserInputDTOConvert().convert(userInputDTO); User saveUserResult = userService.addUser(user);\n变成了: User user = userInputDTO.convertToUser(); User saveUserResult = userService.addUser(user);\n我们再DTO对象中添加了转化的行为，我相信这样的操作可以让代码的可读性变得更强，并且是符合语义的。\n再查工具类 # 再来看DTO内部转化的代码，它实现了我们自己定义的DTOConvert接口，但是这样真的就没有问题，不需要再思考了吗？ 我觉得并不是，对于Convert这种转化语义来讲，很多工具类中都有这样的定义，这中Convert并不是业务级别上的接口定义，它只是用于普通bean之间转化属性值的普通意义上的接口定义，所以我们应该更多的去读其他含有Convert转化语义的代码。 我仔细阅读了一下GUAVA的源码，发现了com.google.common.base.Convert这样的定义:\npublic abstract class Converter\u0026lt;A, B\u0026gt; implements Function\u0026lt;A, B\u0026gt; { protected abstract B doForward(A a); protected abstract A doBackward(B b); //其他略 } 从源码可以了解到，GUAVA中的Convert可以完成正向转化和逆向转化，继续修改我们DTO中转化的这段代码:\nprivate static class UserInputDTOConvert implements DTOConvert\u0026lt;UserInputDTO,User\u0026gt; { @Override public User convert(UserInputDTO userInputDTO) { User user = new User(); BeanUtils.copyProperties(userInputDTO,user); return user; } } 修改后:\nprivate static class UserInputDTOConvert extends Converter\u0026lt;UserInputDTO, User\u0026gt; { @Override protected User doForward(UserInputDTO userInputDTO) { User user = new User(); BeanUtils.copyProperties(userInputDTO,user); return user; } @Override protected UserInputDTO doBackward(User user) { UserInputDTO userInputDTO = new UserInputDTO(); BeanUtils.copyProperties(user,userInputDTO); return userInputDTO; } } 看了这部分代码以后，你可能会问，那逆向转化会有什么用呢？其实我们有很多小的业务需求中，入参和出参是一样的，那么我们变可以轻松的进行转化，我将上边所提到的UserInputDTO和UserOutputDTO都转成UserDTO展示给大家:\nDTO：\npublic class UserDTO { private String username; private int age; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public User convertToUser(){ UserDTOConvert userDTOConvert = new UserDTOConvert(); User convert = userDTOConvert.convert(this); return convert; } public UserDTO convertFor(User user){ UserDTOConvert userDTOConvert = new UserDTOConvert(); UserDTO convert = userDTOConvert.reverse().convert(user); return convert; } private static class UserDTOConvert extends Converter\u0026lt;UserDTO, User\u0026gt; { @Override protected User doForward(UserDTO userDTO) { User user = new User(); BeanUtils.copyProperties(userDTO,user); return user; } @Override protected UserDTO doBackward(User user) { UserDTO userDTO = new UserDTO(); BeanUtils.copyProperties(user,userDTO); return userDTO; } } } api:\n@PostMapping public UserDTO addUser(UserDTO userDTO){ User user = userDTO.convertToUser(); User saveResultUser = userService.addUser(user); UserDTO result = userDTO.convertFor(saveResultUser); return result; } 当然，上述只是表明了转化方向的正向或逆向，很多业务需求的出参和入参的DTO对象是不同的，那么你需要更明显的告诉程序：逆向是无法调用的:\nprivate static class UserDTOConvert extends Converter\u0026lt;UserDTO, User\u0026gt; { @Override protected User doForward(UserDTO userDTO) { User user = new User(); BeanUtils.copyProperties(userDTO,user); return user; } @Override protected UserDTO doBackward(User user) { throw new AssertionError(\u0026#34;不支持逆向转化方法!\u0026#34;); } } 看一下doBackward方法，直接抛出了一个断言异常，而不是业务异常，这段代码告诉代码的调用者，这个方法不是准你调用的，如果你调用，我就”断言”你调用错误了。\n关于异常处理的更详细介绍，可以参考我之前的文章:如何优雅的设计java异常 ，应该可以帮你更好的理解异常。\nbean的验证 # 如果你认为我上边写的那个添加用户api写的已经非常完美了，那只能说明你还不是一个优秀的程序员。我们应该保证任何数据的入参到方法体内都是合法的。\n为什么要验证 # 很多人会告诉我，如果这些api是提供给前端进行调用的，前端都会进行验证啊，你为什还要验证？ 其实答案是这样的，我从不相信任何调用我api或者方法的人，比如前端验证失败了，或者某些人通过一些特殊的渠道(比如Charles进行抓包),直接将数据传入到我的api，那我仍然进行正常的业务逻辑处理，那么就有可能产生脏数据！ “对于脏数据的产生一定是致命”，这句话希望大家牢记在心，再小的脏数据也有可能让你找几个通宵！\njsr 303验证 # hibernate提供的jsr 303实现，我觉得目前仍然是很优秀的，具体如何使用，我不想讲，因为谷歌上你可以搜索出很多答案! 再以上班的api实例进行说明，我们现在对DTO数据进行检查:\npublic class UserDTO { @NotNull private String username; @NotNull private int age; //其他代码略 } api验证:\n@PostMapping public UserDTO addUser(@Valid UserDTO userDTO){ User user = userDTO.convertToUser(); User saveResultUser = userService.addUser(user); UserDTO result = userDTO.convertFor(saveResultUser); return result; } 我们需要将验证结果传给前端，这种异常应该转化为一个api异常(带有错误码的异常)。\n@PostMapping public UserDTO addUser(@Valid UserDTO userDTO, BindingResult bindingResult){ checkDTOParams(bindingResult); User user = userDTO.convertToUser(); User saveResultUser = userService.addUser(user); UserDTO result = userDTO.convertFor(saveResultUser); return result; } private void checkDTOParams(BindingResult bindingResult){ if(bindingResult.hasErrors()){ //throw new 带验证码的验证错误异常 } } BindingResult是Spring MVC验证DTO后的一个结果集，可以参考spring 官方文档\n检查参数后，可以抛出一个“带验证码的验证错误异常”，具体异常设计可以参考如何优雅的设计java异常\n拥抱lombok # 上边的DTO代码，已经让我看的很累了，我相信读者也是一样，看到那么多的Getter和Setter方法，太烦躁了，那时候有什么方法可以简化这些呢。 请拥抱lombok,它会帮助我们解决一些让我们很烦躁的问题\n去掉Setter和Getter # 其实这个标题，我不太想说，因为网上太多，但是因为很多人告诉我，他们根本就不知道lombok的存在，所以为了让读者更好的学习，我愿意写这样一个例子：\n@Setter @Getter public class UserDTO { @NotNull private String username; @NotNull private int age; public User convertToUser(){ UserDTOConvert userDTOConvert = new UserDTOConvert(); User convert = userDTOConvert.convert(this); return convert; } public UserDTO convertFor(User user){ UserDTOConvert userDTOConvert = new UserDTOConvert(); UserDTO convert = userDTOConvert.reverse().convert(user); return convert; } private static class UserDTOConvert extends Converter\u0026lt;UserDTO, User\u0026gt; { @Override protected User doForward(UserDTO userDTO) { User user = new User(); BeanUtils.copyProperties(userDTO,user); return user; } @Override protected UserDTO doBackward(User user) { throw new AssertionError(\u0026#34;不支持逆向转化方法!\u0026#34;); } } } 看到了吧，烦人的Getter和Setter方法已经去掉了。 但是上边的例子根本不足以体现lombok的强大。我希望写一些网上很难查到，或者很少人进行说明的lombok的使用以及在使用时程序语义上的说明。 比如:@Data,@AllArgsConstructor,@NoArgsConstructor..这些我就不进行一一说明了，请大家自行查询资料.\nbean中的链式风格 # 什么是链式风格？我来举个例子，看下面这个Student的bean:\npublic class Student { private String name; private int age; public String getName() { return name; } public Student setName(String name) { this.name = name; return this; } public int getAge() { return age; } public Student setAge(int age) { return this; } } 仔细看一下set方法，这样的设置便是chain的style，调用的时候，可以这样使用:\nStudent student = new Student() .setAge(24) .setName(\u0026#34;zs\u0026#34;); 相信合理使用这样的链式代码，会更多的程序带来很好的可读性，那看一下如果使用lombok进行改善呢，请使用 @Accessors(chain = true),看如下代码:\n@Accessors(chain = true) @Setter @Getter public class Student { private String name; private int age; } 这样就完成了一个对于bean来讲很友好的链式操作。\n静态构造方法 # 静态构造方法的语义和简化程度真的高于直接去new一个对象。比如new一个List对象，过去的使用是这样的:\nList\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); 看一下guava中的创建方式:\nList\u0026lt;String\u0026gt; list = Lists.newArrayList(); Lists命名是一种约定(俗话说：约定优于配置)，它是指Lists是List这个类的一个工具类，那么使用List的工具类去产生List，这样的语义是不是要比直接new一个子类来的更直接一些呢，答案是肯定的，再比如如果有一个工具类叫做Maps，那你是否想到了创建Map的方法呢：\nHashMap\u0026lt;String, String\u0026gt; objectObjectHashMap = Maps.newHashMap(); 好了，如果你理解了我说的语义，那么，你已经向成为java程序员更近了一步了。\n再回过头来看刚刚的Student，很多时候，我们去写Student这个bean的时候，他会有一些必输字段，比如Student中的name字段，一般处理的方式是将name字段包装成一个构造方法，只有传入name这样的构造方法，才能创建一个Student对象。\n接上上边的静态构造方法和必传参数的构造方法，使用lombok将更改成如下写法（@RequiredArgsConstructor 和 @NonNull）:\n@Accessors(chain = true) @Setter @Getter @RequiredArgsConstructor(staticName = \u0026#34;ofName\u0026#34;) public class Student { @NonNull private String name; private int age; } 测试代码:\nStudent student = Student.ofName(\u0026#34;zs\u0026#34;); 这样构建出的bean语义是否要比直接new一个含参的构造方法(包含 name的构造方法)要好很多。\n当然，看过很多源码以后，我想相信将静态构造方法ofName换成of会先的更加简洁:\n@Accessors(chain = true) @Setter @Getter @RequiredArgsConstructor(staticName = \u0026#34;of\u0026#34;) public class Student { @NonNull private String name; private int age; } 测试代码:\nStudent student = Student.of(\u0026#34;zs\u0026#34;); 当然他仍然是支持链式调用的:\nStudent student = Student.of(\u0026#34;zs\u0026#34;).setAge(24); 这样来写代码，真的很简洁，并且可读性很强。\n使用builder # Builder模式我不想再多解释了，读者可以看一下《Head First》(设计模式) 的建造者模式。\n今天其实要说的是一种变种的builder模式，那就是构建bean的builder模式，其实主要的思想是带着大家一起看一下lombok给我们带来了什么。\n看一下Student这个类的原始builder状态:\npublic class Student { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public static Builder builder(){ return new Builder(); } public static class Builder{ private String name; private int age; public Builder name(String name){ this.name = name; return this; } public Builder age(int age){ this.age = age; return this; } public Student build(){ Student student = new Student(); student.setAge(age); student.setName(name); return student; } } } 调用方式:\nStudent student = Student.builder().name(\u0026#34;zs\u0026#34;).age(24).build(); 这样的builder代码，让我是在恶心难受，于是我打算用lombok重构这段代码:\n@Builder public class Student { private String name; private int age; } 调用方式:\nStudent student = Student.builder().name(\u0026#34;zs\u0026#34;).age(24).build(); 代理模式 # 正如我们所知的，在程序中调用rest接口是一个常见的行为动作，如果你和我一样使用过spring 的RestTemplate,我相信你会我和一样，对他抛出的非http状态码异常深恶痛绝。\n所以我们考虑将RestTemplate最为底层包装器进行包装器模式的设计:\npublic abstract class FilterRestTemplate implements RestOperations { protected volatile RestTemplate restTemplate; protected FilterRestTemplate(RestTemplate restTemplate){ this.restTemplate = restTemplate; } //实现RestOperations所有的接口 } 然后再由扩展类对FilterRestTemplate进行包装扩展:\npublic class ExtractRestTemplate extends FilterRestTemplate { private RestTemplate restTemplate; public ExtractRestTemplate(RestTemplate restTemplate) { super(restTemplate); this.restTemplate = restTemplate; } public \u0026lt;T\u0026gt; RestResponseDTO\u0026lt;T\u0026gt; postForEntityWithNoException(String url, Object request, Class\u0026lt;T\u0026gt; responseType, Object... uriVariables) throws RestClientException { RestResponseDTO\u0026lt;T\u0026gt; restResponseDTO = new RestResponseDTO\u0026lt;T\u0026gt;(); ResponseEntity\u0026lt;T\u0026gt; tResponseEntity; try { tResponseEntity = restTemplate.postForEntity(url, request, responseType, uriVariables); restResponseDTO.setData(tResponseEntity.getBody()); restResponseDTO.setMessage(tResponseEntity.getStatusCode().name()); restResponseDTO.setStatusCode(tResponseEntity.getStatusCodeValue()); }catch (Exception e){ restResponseDTO.setStatusCode(RestResponseDTO.UNKNOWN_ERROR); restResponseDTO.setMessage(e.getMessage()); restResponseDTO.setData(null); } return restResponseDTO; } } 包装器ExtractRestTemplate很完美的更改了异常抛出的行为，让程序更具有容错性。在这里我们不考虑ExtractRestTemplate完成的功能，让我们把焦点放在FilterRestTemplate上，“实现RestOperations所有的接口”,这个操作绝对不是一时半会可以写完的，当时在重构之前我几乎写了半个小时,如下:\npublic abstract class FilterRestTemplate implements RestOperations { protected volatile RestTemplate restTemplate; protected FilterRestTemplate(RestTemplate restTemplate) { this.restTemplate = restTemplate; } @Override public \u0026lt;T\u0026gt; T getForObject(String url, Class\u0026lt;T\u0026gt; responseType, Object... uriVariables) throws RestClientException { return restTemplate.getForObject(url,responseType,uriVariables); } @Override public \u0026lt;T\u0026gt; T getForObject(String url, Class\u0026lt;T\u0026gt; responseType, Map\u0026lt;String, ?\u0026gt; uriVariables) throws RestClientException { return restTemplate.getForObject(url,responseType,uriVariables); } @Override public \u0026lt;T\u0026gt; T getForObject(URI url, Class\u0026lt;T\u0026gt; responseType) throws RestClientException { return restTemplate.getForObject(url,responseType); } @Override public \u0026lt;T\u0026gt; ResponseEntity\u0026lt;T\u0026gt; getForEntity(String url, Class\u0026lt;T\u0026gt; responseType, Object... uriVariables) throws RestClientException { return restTemplate.getForEntity(url,responseType,uriVariables); } //其他实现代码略。。。 } 我相信你看了以上代码，你会和我一样觉得恶心反胃，后来我用lombok提供的代理注解优化了我的代码(@Delegate):\n@AllArgsConstructor public abstract class FilterRestTemplate implements RestOperations { @Delegate protected volatile RestTemplate restTemplate; } 这几行代码完全替代上述那些冗长的代码。 是不是很简洁，做一个拥抱lombok的程序员吧。\n重构 # 需求案例 # 项目需求 # 项目开发阶段，有一个关于下单发货的需求：如果今天下午3点前进行下单，那么发货时间是明天，如果今天下午3点后进行下单，那么发货时间是后天，如果被确定的时间是周日，那么在此时间上再加1天为发货时间。\n思考与重构 # 我相信这个需求看似很简单，无论怎么写都可以完成。\n很多人可能看到这个需求，就动手开始写Calendar或Date进行计算，从而完成需求。\n而我给的建议是，仔细考虑如何写代码，然后再去写，不是说所有的时间操作都用Calendar或Date去解决，一定要看场景。\n对于时间的计算我们要考虑joda-time这种类似的成熟时间计算框架来写代码，它会让代码更加简洁和易读。\n请读者先考虑这个需求如何用java代码完成，或先写一个你觉得完成这个代码的思路，再来看我下边的代码，这样，你的收获会更多一些:\nfinal DateTime DISTRIBUTION_TIME_SPLIT_TIME = new DateTime().withTime(15,0,0,0); private Date calculateDistributionTimeByOrderCreateTime(Date orderCreateTime){ DateTime orderCreateDateTime = new DateTime(orderCreateTime); Date tomorrow = orderCreateDateTime.plusDays(1).toDate(); Date theDayAfterTomorrow = orderCreateDateTime.plusDays(2).toDate(); return orderCreateDateTime.isAfter(DISTRIBUTION_TIME_SPLIT_TIME) ? wrapDistributionTime(theDayAfterTomorrow) : wrapDistributionTime(tomorrow); } private Date wrapDistributionTime(Date distributionTime){ DateTime currentDistributionDateTime = new DateTime(distributionTime); DateTime plusOneDay = currentDistributionDateTime.plusDays(1); boolean isSunday = (DateTimeConstants.SUNDAY == currentDistributionDateTime.getDayOfWeek()); return isSunday ? plusOneDay.toDate() : currentDistributionDateTime.toDate() ; } 读这段代码的时候，你会发现，我将判断和有可能出现的不同结果都当做一个变量，最终做一个三目运算符的方式进行返回，这样的优雅和可读性显而易见，当然这样的代码不是一蹴而就的，我优化了3遍产生的以上代码。读者可根据自己的代码和我写的代码进行对比。\n提高方法 # 如果你做了3年+的程序员，我相信像如上这样的需求，你很轻松就能完成，但是如果你想做一个会写java的程序员，就好好的思考和重构代码吧。 写代码就如同写字一样，同样的字，大家都会写，但是写出来是否好看就不一定了。如果想把程序写好，就要不断的思考和重构，敢于尝试，敢于创新，不要因循守旧，一定要做一个优秀的java程序员。 提高代码水平最好的方法就是有条理的重构！(注意：是有条理的重构)\n设计模式 # 设计模式就是工具，而不是提现你是否是高水平程序员的一个指标。\n我经常会看到某一个程序员兴奋的大喊，哪个程序哪个点我用到了设计模式，写的多么多么优秀，多么多么好。我仔细去翻阅的时候，却发现有很多是过度设计的。\n业务驱动技术 or 技术驱动业务 # 业务驱动技术 or 技术驱动业务 ？ 其实这是一个一直在争论的话题，但是很多人不这么认为，我觉得就是大家不愿意承认罢了。我来和大家大概分析一下作为一个java程序员，我们应该如何判断自己所处于的位置.\n业务驱动技术：如果你所在的项目是一个收益很小或者甚至没有收益的项目，请不要搞其他创新的东西，不要驱动业务要如何如何做，而是要熟知业务现在的痛点是什么？如何才能帮助业务盈利或者让项目更好，更顺利的进行。\n技术驱动业务：如果你所在的项目是一个很牛的项目，比如淘宝这类的项目，我可以在满足业务需求的情况下，和业务沟通，使用什么样的技术能更好的帮助业务创造收益，比如说下单的时候要进队列，可能几分钟之后订单状态才能处理完成，但是会让用户有更流畅的体验，赚取更多的访问流量，那么我相信业务愿意被技术驱动，会同意订单的延迟问题，这样便是技术驱动业务。\n我相信大部分人还都处于业务驱动技术的方向吧。\n所以你既然不能驱动业务，那就请拥抱业务变化吧。\n代码设计 # 一直在做java后端的项目，经常会有一些变动，我相信大家也都遇到过。\n比如当我们写一段代码的时候，我们考虑将需求映射成代码的状态模式，突然有一天，状态模式里边又添加了很多行为变化的东西，这时候你就挠头了，你硬生生的将状态模式中添加过多行为和变化。\n慢慢的你会发现这些状态模式，其实更像是一簇算法，应该使用策略模式，这时你应该已经晕头转向了。\n说了这么多，我的意思是，只要你觉得合理，就请将状态模式改为策略模式吧，所有的模式并不是凭空想象出来的，都是基于重构。\njava编程中没有银弹，请拥抱业务变化，一直思考重构，你就有一个更好的代码设计!\n你真的优秀吗？ # 真不好意思，我取了一个这么无聊的标题。\n国外流行一种编程方式，叫做结对编程，我相信国内很多公司都没有这么做，我就不在讲述结对编程带来的好处了，其实就是一边code review，一边互相提高的一个过程。既然做不到这个，那如何让自己活在自己的世界中不断提高呢？\n“平时开发的时候，做出的代码总认为是正确的，而且写法是完美的。”，我相信这是大部分人的心声，还回到刚刚的问题，如何在自己的世界中不断提高呢？\n答案就是:\n多看成熟框架的源码 多回头看自己的代码 勤于重构 你真的优秀吗？ 如果你每周都完成了学习源码，回头看自己代码，然后勤于重构，我认为你就真的很优秀了。\n即使也许你只是刚刚入门，但是一直坚持，你就是一个真的会写java代码的程序员了。\n技能 # UML # 不想多讨论UML相关的知识，但是我觉得你如果真的会写java，请先学会表达自己，UML就是你说话的语言，做一名优秀的java程序员，请至少学会这两种UML图：\n类图 时序图 clean code # 我认为保持代码的简洁和可读性是代码的最基本保证，如果有一天为了程序的效率而降低了这两点，我认为是可以谅解的，除此之外，没有任何理由可以让你任意挥霍你的代码。\n读者可以看一下Robert C. Martin出版的《Clean Code》（代码整洁之道） 这本书 可以参考美团文章聊聊clean code 也可以看一下阿里的Java编码规范 无论如何，请保持你的代码的整洁。\nlinux 基础命令 # 这点其实和会写java没有关系，但是linux很多时候确实承载运行java的容器，请学好linux的基础命令。\n参考鸟哥的《Linux私房菜》 总结 # java是一个大体系，今天讨论并未涉及框架和架构相关知识，只是讨论如何写好代码。\n本文从写java程序的小方面一直写到大方面，来阐述了如何才能写好java程序，并告诉读者们如何才能提高自身的编码水平。\n我希望看到这篇文章的各位都能做一个优秀的java程序员。\n","date":"2021-07-22","externalUrl":null,"permalink":"/2021/can_you_write_java/","section":"Posts","summary":"Java 最佳实践","title":"细思极恐-你真的会写java吗?","type":"posts"},{"content":" [TOC]\n一、数据类型 # 基本类型 # 基本类型 大小 最小值 最大值 默认值 包装器类型 boolean - - - false Boolean byte 8 -128 127 (byte)0 Byte char 16 Unicode 0 Unicode 65535 \\u0000 Character short 16 -32768 32767 (short)0 Short int 32 -231 231-1 0 Integer、BigInteger long 64 -263 263-1 0L Long float 32 IEEE754 IEEE754 0.0f Float、BigDecimal double 64 IEEE754 IEEE754 0.0d, 0.0 Double boolean 只有两个值：true、false，可以使用 1 bit 来存储，但是具体大小没有明确规定。JVM 会在编译时期将 boolean 类型的数据转换为 int，使用 1 来表示 true，0 表示 false。JVM 支持 boolean 数组，但通过读写 byte 数组来实现的。\nchar （采用UTF-16LE编码Unicode字符）中只能放 UTF-16 编码下只占 2 字节的那些字符\nPrimitive Data Types The Java® Virtual Machine Specification 包装类型 # 基本类型都有对应的包装类型，基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。包装类型是不可变对象！\nInteger x = 2; // 装箱 调用了 Integer.valueOf(2) int y = x; // 拆箱 调用了 X.intValue() ++x; // 先拆箱，在自增，最后在装箱，注意：操作完成后，x将引用另外一个Integer对象 Autoboxing and Unboxing 缓存池 # new Integer(123) 与 Integer.valueOf(123) 的区别在于：\nnew Integer(123) 每次都会新建一个对象； Integer.valueOf(123) 会使用缓存池中的对象，多次调用会取得同一个对象的引用。 Integer x = new Integer(123); Integer y = new Integer(123); System.out.println(x == y); // false Integer z = Integer.valueOf(123); Integer k = Integer.valueOf(123); System.out.println(z == k); // true valueOf() 方法的实现比较简单，就是先判断值是否在缓存池中，如果在的话就直接返回缓存池的内容。\npublic static Integer valueOf(int i) { if (i \u0026gt;= IntegerCache.low \u0026amp;\u0026amp; i \u0026lt;= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } 在 Java 8 中，Integer 缓存池的大小默认为 -128~127。\nstatic final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty(\u0026#34;java.lang.Integer.IntegerCache.high\u0026#34;); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k \u0026lt; cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high \u0026gt;= 127; } 编译器会在自动装箱过程调用 valueOf() 方法，因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建，那么就会引用相同的对象。\nInteger m = 123; Integer n = 123; System.out.println(m == n); // true 基本类型对应的缓冲池如下：\nboolean values true and false all byte values short values between -128 and 127 int values between -128 and 127 char in the range \\u0000 to \\u007F 在使用这些基本类型对应的包装类型时，如果该数值范围在缓冲池范围内，就可以直接使用缓冲池中的对象。\n在 jdk 1.8 所有的数值类缓冲池中，Integer 的缓冲池 IntegerCache 很特殊，这个缓冲池的下界是 - 128，上界默认是 127，但是这个上界是可调的，在启动 jvm 的时候，通过 -XX:AutoBoxCacheMax=\u0026lt;size\u0026gt; 来指定这个缓冲池的大小，该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性，然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界。\nStackOverflow : Differences between new Integer(123), Integer.valueOf(123) and just 123 参数传递 # 方法类型：签名由\nJava 的参数是以值传递的形式传入方法中，而不是引用传递。\n以下代码中 Dog dog 的 dog 是一个指针，存储的是对象的地址。在将一个参数传入一个方法时，本质上是将对象的地址以值的方式传递到形参中。\npublic class Dog { String name; Dog(String name) { this.name = name; } String getName() { return this.name; } void setName(String name) { this.name = name; } String getObjectAddress() { return super.toString(); } } 在方法中改变对象的字段值会改变原对象该字段值，因为引用的是同一个对象。\nclass PassByValueExample { public static void main(String[] args) { Dog dog = new Dog(\u0026#34;A\u0026#34;); func(dog); System.out.println(dog.getName()); // B } private static void func(Dog dog) { dog.setName(\u0026#34;B\u0026#34;); } } 但是在方法中将指针引用了其它对象，那么此时方法里和方法外的两个指针指向了不同的对象，在一个指针改变其所指向对象的内容对另一个指针所指向的对象没有影响。\npublic class PassByValueExample { public static void main(String[] args) { Dog dog = new Dog(\u0026#34;A\u0026#34;); System.out.println(dog.getObjectAddress()); // Dog@4554617c func(dog); System.out.println(dog.getObjectAddress()); // Dog@4554617c System.out.println(dog.getName()); // A } private static void func(Dog dog) { System.out.println(dog.getObjectAddress()); // Dog@4554617c dog = new Dog(\u0026#34;B\u0026#34;); System.out.println(dog.getObjectAddress()); // Dog@74a14482 System.out.println(dog.getName()); // B } } StackOverflow: Is Java “pass-by-reference” or “pass-by-value”?\n类型转换 # 强制类型转换：显式向下转型 (SubClass)baseRef，其正确性由Run-Time Type Identification`机制保证\n自动类型转换：Java 仅支持隐式向上转型，不能隐式执行向下转型，因为这会使得精度降低。但有一个例外： TODO: 自动包装\nTODO: toString()自动类型转换\n因为字面量 1 是 int 类型，它比 short 类型精度要高，因此不能隐式地将 int 类型向下转型为 short 类型。\nshort s1 = 1; //! s1 = s1 + 1; 但是使用 += 或者 ++ 运算符会执行隐式类型转换。\ns1 += 1; s1++; 上面的语句相当于将 s1 + 1 的计算结果进行了显式向下转型：\ns1 = (short) (s1 + 1); StackOverflow : Why don\u0026rsquo;t Java\u0026rsquo;s +=, -=, *=, /= compound assignment operators require casting?\n类对象初始化 # new 分配对象空间，并执行成员变量默认初始化 执行成员变量显式初始化 调用构造方法：调用构造器之前，所有数据成员按定义顺序依次初始化 返回对象地址 二、String # 字符集与编码 # 字符集 编号(code point) 编码(encoding) 用途 BOM 拉丁字母 ASCII ASCII 简体中文 GB2312 GB2312 所有字符 Unicode UTF-8 变长，兼容ASCII、适合存储、传输 EFBBBF(统一用大端BE) 使用代理区表示Unicode字符 0x10FFFF UTF-16 **定长，**省空间、用于内存表示 FEFF(BE)、FFFE(LE) 使用4字节表示Unicode字符 Unicode UTF-32 直观表示所有Unicode，但浪费空间 \\uFEFF 字符、Unicode、UTF-8、UTF-16、UTF-32相互编码和解码。一次可连续输入64个字符；编码也是连续输入，但需注意UTF-16/32的字节顺序标记（BOM），如果没有提供BOM，默认以大尾序解码。 Unicode 是容纳世界所有文字符号的国际标准编码，使用四个字节为每个字符编码。 UTF 是英文 Unicode Transformation Format 的缩写，意为把 Unicode 字符转换为某种格式。UTF 系列编码方案（UTF-8、UTF-16、UTF-32）均是由 Unicode 编码方案衍变而来，以适应不同的数据存储或传递，它们都可以完全表示 Unicode 标准中的所有字符。目前，这些衍变方案中 UTF-8 被广泛使用，而 UTF-16 和 UTF-32 则很少被使用。 UTF-8 使用一至四个字节为每个字符编码，其中大部分汉字采用三个字节编码，少量不常用汉字采用四个字节编码。因为 UTF-8 是可变长度的编码方式，相对于 Unicode 编码可以减少存储占用的空间，所以被广泛使用。 UTF-16 使用二或四个字节为每个字符编码，其中大部分汉字采用两个字节编码，少量不常用汉字采用四个字节编码。UTF-16 编码有大尾序和小尾序之别，即 UTF-16BE 和 UTF-16LE，在编码前会放置一个 U+FEFF 或 U+FFFE（UTF-16BE 以 FEFF 代表，UTF-16LE 以 FFFE 代表），其中 U+FEFF 字符在 Unicode 中代表的意义是 ZERO WIDTH NO-BREAK SPACE，顾名思义，它是个没有宽度也没有断字的空白。 UTF-32 使用四个字节为每个字符编码，使得 UTF-32 占用空间通常会是其它编码的二到四倍。UTF-32 与 UTF-16 一样有大尾序和小尾序之别，编码前会放置 U+0000FEFF 或 U+FFFE0000 以区分。 字符集与编码（四）——Unicode 概览 # String 被声明为 final，因此它不可被继承。(Integer 等包装类也不能被继承）\n在 Java 8 中，String 内部使用 char 数组存储数据。\npublic final class String implements java.io.Serializable, Comparable\u0026lt;String\u0026gt;, CharSequence { /** The value is used for character storage. */ private final char value[]; } 在 Java 9 之后，String 类的实现改用 byte 数组存储字符串，同时使用 coder 来标识使用了哪种编码。\npublic final class String implements java.io.Serializable, Comparable\u0026lt;String\u0026gt;, CharSequence { /** The value is used for character storage. */ private final byte[] value; /** The identifier of the encoding used to encode the bytes in {@code value}. */ private final byte coder; } value 数组被声明为 final，这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法，因此可以保证 String 不可变。\n不可变的好处 # 1. 可以缓存 hash 值\n因为 String 的 hash 值经常被使用，例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变，因此只需要进行一次计算。\n2. String Pool 的需要\n如果一个 String 对象已经被创建过了，那么就会从 String Pool 中取得引用。只有 String 是不可变的，才可能使用 String Pool。\n3. 安全性\nString 经常作为参数，String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的，那么在网络连接过程中，String 被改变，改变 String 的那一方以为现在连接的是其它主机，而实际情况却不一定是。\n4. 线程安全\nString 不可变性天生具备线程安全，可以在多个线程中安全地使用。\nProgram Creek : Why String is immutable in Java?\nString, StringBuffer and StringBuilder # 1. 可变性\nString 不可变 StringBuffer 和 StringBuilder 可变 2. 线程安全\nString 不可变，因此是线程安全的 StringBuilder 线程不安全，但是效率高 StringBuffer 是线程安全的，内部使用 synchronized 进行同步 StackOverflow : String, StringBuffer, and StringBuilder\nString Pool # 字符串常量池（String Pool）保存着所有字符串字面量（literal strings），这些字面量在编译时期就确定。不仅如此，还可以使用 String 的 intern() 方法在运行过程将字符串添加到 String Pool 中。\n当一个字符串调用 intern() 方法时，如果 String Pool 中已经存在一个字符串和该字符串值相等（使用 equals() 方法进行确定），那么就会返回 String Pool 中字符串的引用；否则，就会在 String Pool 中添加一个新的字符串，并返回这个新字符串的引用。\n下面示例中，s1 和 s2 采用 new String() 的方式新建了两个不同字符串，而 s3 和 s4 是通过 s1.intern() 方法取得同一个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中，然后返回这个字符串引用。因此 s3 和 s4 引用的是同一个字符串。\nString s1 = new String(\u0026#34;aaa\u0026#34;); String s2 = new String(\u0026#34;aaa\u0026#34;); System.out.println(s1 == s2); // false String s3 = s1.intern(); String s4 = s1.intern(); System.out.println(s3 == s4); // true 如果是采用 \u0026ldquo;bbb\u0026rdquo; 这种字面量的形式创建字符串，会自动地将字符串放入 String Pool 中。\nString s5 = \u0026#34;bbb\u0026#34;; String s6 = \u0026#34;bbb\u0026#34;; System.out.println(s5 == s6); // true 在 Java 7 之前，String Pool 被放在运行时常量池中，它属于永久代。而在 Java 7，String Pool 被移到堆中。这是因为永久代的空间有限，在大量使用字符串的场景下会导致 OutOfMemoryError 错误。\nStackOverflow : What is String interning? 深入解析 String#intern new String(\u0026ldquo;abc\u0026rdquo;) # 使用这种方式一共会创建两个字符串对象（前提是 String Pool 中还没有 \u0026ldquo;abc\u0026rdquo; 字符串对象）。\n\u0026ldquo;abc\u0026rdquo; 属于字符串字面量，因此编译时期会在 String Pool 中创建一个字符串对象，指向这个 \u0026ldquo;abc\u0026rdquo; 字符串字面量； 而使用 new 的方式会在堆中创建一个字符串对象。 创建一个测试类，其 main 方法中使用这种方式来创建字符串对象。\npublic class NewStringTest { public static void main(String[] args) { String s = new String(\u0026#34;abc\u0026#34;); } } 使用 javap -verbose 进行反编译，得到以下内容：\n// ... Constant pool: // ... #2 = Class #18 // java/lang/String #3 = String #19 // abc // ... #18 = Utf8 java/lang/String #19 = Utf8 abc // ... public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=2, args_size=1 0: new #2 // class java/lang/String 3: dup 4: ldc #3 // String abc 6: invokespecial #4 // Method java/lang/String.\u0026#34;\u0026lt;init\u0026gt;\u0026#34;:(Ljava/lang/String;)V 9: astore_1 // ... 在 Constant Pool 中，#19 存储这字符串字面量 \u0026ldquo;abc\u0026rdquo;，#3 是 String Pool 的字符串对象，它指向 #19 这个字符串字面量。在 main 方法中，0: 行使用 new #2 在堆中创建一个字符串对象，并且使用 ldc #3 将 String Pool 中的字符串对象作为 String 构造函数的参数。\n以下是 String 构造函数的源码，可以看到，在将一个字符串对象作为另一个字符串对象的构造函数参数时，并不会完全复制 value 数组内容，而是都会指向同一个 value 数组。\npublic String(String original) { this.value = original.value; this.hash = original.hash; } 格式化输出 # System.out.printf(\u0026#34;格式说明符\u0026#34;, var1, var2...); System.out.format(\u0026#34;格式说明符\u0026#34;, var1, var2...); 格式说明符 %[arguementIndex$][flags][width][.precision]conversion\narguementIndex$：参数占位符 flags：对齐控制，默认右对齐，-为左对齐 width：控制域最小尺寸 .precision：控制域最大尺寸，不能用于整型 conversion：格式转换 conversion meaning d 十进制整数 x 十六进制整数 h 十六进制hash值 % % f 十进制浮点数 e 科学技术浮点数 b boolean值 c Unicode字符 s 字符串 正则表达式 # 三、时间相关类 # long now = System.currentTimeMillis(); Date date = new Date(now); // 精确到毫秒 DateFormat df = new SimpleDateFormat(\u0026#34;yyyy-MM-dd hh:mm:ss\u0026#34;) String daytime = df.format(date); Date date = df.parse(\u0026#34;2007-10-7 20:15:30\u0026#34;) Calendar calendar = new GregorianCalendar(); 四、关键字 # switch # 从 Java 7 开始，可以在 switch 条件判断语句中使用 String 对象。\nString s = \u0026#34;a\u0026#34;; switch (s) { case \u0026#34;a\u0026#34;: System.out.println(\u0026#34;aaa\u0026#34;); break; case \u0026#34;b\u0026#34;: System.out.println(\u0026#34;bbb\u0026#34;); break; } switch 不支持 long，是因为 switch 的设计初衷是对那些只有少数几个值的类型进行等值判断，如果值过于复杂，那么还是用 if 比较合适。\n// long x = 111; // switch (x) { // Incompatible types. Found: \u0026#39;long\u0026#39;, required: \u0026#39;char, byte, short, int, Character, Byte, Short, Integer, String, or an enum\u0026#39; // case 111: // System.out.println(111); // break; // case 222: // System.out.println(222); // break; // } StackOverflow : Why can\u0026rsquo;t your switch statement data type be long, Java?\nfinal # 1. 数据\n声明数据为常量，可以是编译时常量，也可以是在运行时（用一个方法赋值、构造方法内赋值）被初始化后不能被改变的常量。\n对于基本类型，final 使数值不变； 对于引用类型，final 使引用不变，也就不能引用其它对象，但是被引用的对象本身是可以修改的。 final int x = 1; // x = 2; // cannot assign value to final variable \u0026#39;x\u0026#39; final A y = new A(); y.a = 1; 2. 方法\n声明方法不能被子类重写，只能继承。\nprivate 方法隐式地被指定为 final，如果在子类中定义的方法和基类中的一个 private 方法签名相同，此时子类的方法不是重写基类方法，而是在子类中定义了一个新的方法。\n3. 类\n声明类不允许被继承。\nstatic # 1. 静态变量\n静态变量：又称为类变量，也就是说这个变量属于类的，类所有的实例都共享静态变量，可以直接通过类名来访问它。静态变量在内存中只存在一份。 实例变量：每创建一个实例就会产生一个实例变量，它与该实例同生共死。 public class A { private int x; // 实例变量 private static int y; // 静态变量 public static void main(String[] args) { // int x = A.x; // Non-static field \u0026#39;x\u0026#39; cannot be referenced from a static context A a = new A(); int x = a.x; int y = A.y; } } 2. 静态方法\n静态方法在类加载的时候就存在了，它不依赖于任何实例。所以静态方法必须有实现，也就是说它不能是抽象方法。\npublic abstract class A { public static void func1(){ } // Illegal combination of modifiers: \u0026#39;abstract\u0026#39; and \u0026#39;static\u0026#39; // public abstract static void func2(); } 只能访问所属类的静态字段和静态方法，方法中不能有 this 和 super 关键字，因此这两个关键字与具体对象关联；也不能直接调用非静态方法。\npublic class A { private static int x; private int y; public static void func1(){ int a = x; // int b = y; // Non-static field \u0026#39;y\u0026#39; cannot be referenced from a static context // int b = this.y; // \u0026#39;A.this\u0026#39; cannot be referenced from a static context } } 3. 静态语句块\n静态语句块在类初始化时运行一次。\npublic class A { static { System.out.println(\u0026#34;123\u0026#34;); } public static void main(String[] args) { A a1 = new A(); A a2 = new A(); } } 123 4. 静态内部类\n非静态内部类依赖于外部类的实例，也就是说需要先创建外部类实例，才能用这个实例去创建非静态内部类。而静态内部类不需要。\npublic class OuterClass { class InnerClass { } static class StaticInnerClass { } public static void main(String[] args) { // InnerClass innerClass = new InnerClass(); // \u0026#39;OuterClass.this\u0026#39; cannot be referenced from a static context OuterClass outerClass = new OuterClass(); InnerClass innerClass = outerClass.new InnerClass(); StaticInnerClass staticInnerClass = new StaticInnerClass(); } } 静态内部类不能访问外部类的非静态的变量和方法。\n5. 静态导包\n在使用静态变量和方法时不用再指明 ClassName，从而简化代码，但可读性大大降低。\nimport static com.xxx.ClassName.* 6. 初始化顺序\n静态变量和静态语句块优先于实例变量和普通语句块，静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。\npublic static String staticField = \u0026#34;静态变量\u0026#34;; static { System.out.println(\u0026#34;静态语句块\u0026#34;); } public String field = \u0026#34;实例变量\u0026#34;; { System.out.println(\u0026#34;普通语句块\u0026#34;); } // 可在此语句块内进行实例初始化 最后才是构造函数的初始化。\npublic InitialOrderTest() { System.out.println(\u0026#34;构造函数\u0026#34;); } 存在继承的情况下，初始化顺序为：\n父类（静态变量、静态语句块）//初次访问类中静态成员加载类，同时进行static初始 子类（静态变量、静态语句块） // 静态块在类加载时执行 父类（实例变量、普通语句块）//创建类实例时 父类（构造函数） 子类（实例变量、普通语句块） 子类（构造函数） 五、Object 通用方法 # 概览 # public native int hashCode() public boolean equals(Object obj) protected native Object clone() throws CloneNotSupportedException public String toString() public final native Class\u0026lt;?\u0026gt; getClass() protected void finalize() throws Throwable {} public final native void notify() public final native void notifyAll() public final native void wait(long timeout) throws InterruptedException public final void wait(long timeout, int nanos) throws InterruptedException public final void wait() throws InterruptedException equals() # 1. 等价关系\n两个对象具有等价关系，需要满足以下五个条件：\nⅠ 自反性\nx.equals(x); // true Ⅱ 对称性\nx.equals(y) == y.equals(x); // true Ⅲ 传递性\nif (x.equals(y) \u0026amp;\u0026amp; y.equals(z)) x.equals(z); // true; Ⅳ 一致性\n多次调用 equals() 方法结果不变\nx.equals(y) == x.equals(y); // true Ⅴ 与 null 的比较\n对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false\nx.equals(null); // false; 2. 等价与相等\n对于基本类型，== 判断两个值是否相等，基本类型没有 equals() 方法。 对于引用类型，== 判断两个变量是否引用同一个对象，而 equals() 判断引用的对象是否等价。 Integer x = new Integer(1); Integer y = new Integer(1); System.out.println(x.equals(y)); // true System.out.println(x == y); // false 3. 实现\n检查是否为同一个对象的引用，如果是直接返回 true； 检查是否是同一个类型，如果不是，直接返回 false； 将 Object 对象进行转型； 判断每个关键域是否相等。 public class EqualExample { private int x; private int y; private int z; public EqualExample(int x, int y, int z) { this.x = x; this.y = y; this.z = z; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; EqualExample that = (EqualExample) o; if (x != that.x) return false; if (y != that.y) return false; return z == that.z; } } hashCode() # hashCode() 返回哈希值，而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同，但是散列值相同的两个对象不一定等价，这是因为计算哈希值具有随机性，两个值不同的对象可能计算出相同的哈希值。\n在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法，保证等价的两个对象哈希值也相等。\nHashSet 和 HashMap 等集合类使用了 hashCode() 方法来计算对象应该存储的位置，因此要将对象添加到这些集合类中，需要让对应的类实现 hashCode() 方法。\n下面的代码中，新建了两个等价的对象，并将它们添加到 HashSet 中。我们希望将这两个对象当成一样的，只在集合中添加一个对象。但是 EqualExample 没有实现 hashCode() 方法，因此这两个对象的哈希值是不同的，最终导致集合添加了两个等价的对象。\nEqualExample e1 = new EqualExample(1, 1, 1); EqualExample e2 = new EqualExample(1, 1, 1); System.out.println(e1.equals(e2)); // true HashSet\u0026lt;EqualExample\u0026gt; set = new HashSet\u0026lt;\u0026gt;(); set.add(e1); set.add(e2); System.out.println(set.size()); // 2 理想的哈希函数应当具有均匀性，即不相等的对象应当均匀分布到所有可能的哈希值上。这就要求了哈希函数要把所有域的值都考虑进来。可以将每个域都当成 R 进制的某一位，然后组成一个 R 进制的整数。\nR 一般取 31，因为它是一个奇素数，如果是偶数的话，当出现乘法溢出，信息就会丢失，因为与 2 相乘相当于向左移一位，最左边的位丢失。并且一个数与 31 相乘可以转换成移位和减法：31*x == (x\u0026lt;\u0026lt;5)-x，编译器会自动进行这个优化。\n@Override public int hashCode() { int result = 17; result = 31 * result + x; result = 31 * result + y; result = 31 * result + z; return result; } toString() # 支持任何类向string的转换\n默认返回 ToStringExample@4554617c 这种形式，其中 @ 后面的数值为散列码的无符号十六进制表示。\npublic class ToStringExample { private int number; public ToStringExample(int number) { this.number = number; } } ToStringExample example = new ToStringExample(123); System.out.println(example.toString()); ToStringExample@4554617c clone() # 1. cloneable\nclone() 是 Object 的 protected 方法，它不是 public，一个类不显式去重写 clone()，其它类就不能直接去调用该类实例的 clone() 方法。\npublic class CloneExample { private int a; private int b; } CloneExample e1 = new CloneExample(); // CloneExample e2 = e1.clone(); // \u0026#39;clone()\u0026#39; has protected access in \u0026#39;java.lang.Object\u0026#39; 重写 clone() 得到以下实现：\npublic class CloneExample { private int a; private int b; @Override public CloneExample clone() throws CloneNotSupportedException { return (CloneExample)super.clone(); } } CloneExample e1 = new CloneExample(); try { CloneExample e2 = e1.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } java.lang.CloneNotSupportedException: CloneExample 以上抛出了 CloneNotSupportedException，这是因为 CloneExample 没有实现 Cloneable 接口。\n应该注意的是，clone() 方法并不是 Cloneable 接口的方法，而是 Object 的一个 protected 方法。Cloneable 接口只是规定，如果一个类没有实现 Cloneable 接口又调用了 clone() 方法，就会抛出 CloneNotSupportedException。\npublic class CloneExample implements Cloneable { private int a; private int b; @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } } 2. 浅拷贝\n拷贝对象和原始对象的引用类型引用同一个对象。\npublic class ShallowCloneExample implements Cloneable { private int[] arr; public ShallowCloneExample() { arr = new int[10]; for (int i = 0; i \u0026lt; arr.length; i++) { arr[i] = i; } } public void set(int index, int value) { arr[index] = value; } public int get(int index) { return arr[index]; } @Override protected ShallowCloneExample clone() throws CloneNotSupportedException { return (ShallowCloneExample) super.clone(); } } ShallowCloneExample e1 = new ShallowCloneExample(); ShallowCloneExample e2 = null; try { e2 = e1.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } e1.set(2, 222); System.out.println(e2.get(2)); // 222 3. 深拷贝\n拷贝对象和原始对象的引用类型引用不同对象。\npublic class DeepCloneExample implements Cloneable { private int[] arr; public DeepCloneExample() { arr = new int[10]; for (int i = 0; i \u0026lt; arr.length; i++) { arr[i] = i; } } public void set(int index, int value) { arr[index] = value; } public int get(int index) { return arr[index]; } @Override protected DeepCloneExample clone() throws CloneNotSupportedException { DeepCloneExample result = (DeepCloneExample) super.clone(); result.arr = new int[arr.length]; for (int i = 0; i \u0026lt; arr.length; i++) { result.arr[i] = arr[i]; } return result; } } DeepCloneExample e1 = new DeepCloneExample(); DeepCloneExample e2 = null; try { e2 = e1.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } e1.set(2, 222); System.out.println(e2.get(2)); // 2 4. clone() 的替代方案\n使用 clone() 方法来拷贝一个对象即复杂又有风险，它会抛出异常，并且还需要类型转换。Effective Java 书上讲到，最好不要去使用 clone()，可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。\npublic class CloneConstructorExample { private int[] arr; public CloneConstructorExample() { arr = new int[10]; for (int i = 0; i \u0026lt; arr.length; i++) { arr[i] = i; } } public CloneConstructorExample(CloneConstructorExample original) { arr = new int[original.arr.length]; for (int i = 0; i \u0026lt; original.arr.length; i++) { arr[i] = original.arr[i]; } } public void set(int index, int value) { arr[index] = value; } public int get(int index) { return arr[index]; } } CloneConstructorExample e1 = new CloneConstructorExample(); CloneConstructorExample e2 = new CloneConstructorExample(e1); e1.set(2, 222); System.out.println(e2.get(2)); // 2 finalize() # finalize、垃圾回收机制、System.gc()、引用计数、stop-and-copy停止复制、复制式回收器、自适应分代的、标记-清扫（mark-and-sweep）、即时编译器（JIT）\n六、面向对象 # OOP概念 # 数据抽象与封装： 创建新的数据类型：将数据与方法包装进类 用权限控制隐藏具体实现：接口与实现分离、防止用户误改内部机制 复用类： 继承：is a，复用接口，通过覆盖方法表达行为差异 组合：has a，使用对象，用字段表达状态上的变化 优先使用组合 多态： 动态绑定，消除类型耦合关系，使程序可拓展 分离做什么与怎么做，将接口与实现分离 改善代码结构和可读性 绑定：将一个方法调用与方法主体关联（符号引用\u0026ndash;\u0026gt;直接引用） 前期/静态绑定：程序执行前由编译器或链接程序实现绑定，静态方法、私有方法（子类不可见，隐含final语义，因此不可被继承，就不是虚方法），实例构造方法（不可被继承）；此类方法编译器可知，在编译期完成绑定（符号引用\u0026ndash;\u0026gt;直接引用），运行期不可改变。 后期/动态绑定：运行时根据对象类型进行绑定，Java中对域的访问都是在编译器进行静态绑定的，除了final、static方法外均采用动态绑定，我们称运行期间将符号引用转化为直接引用的方法为虚方法。 设计模式 类文件组织 # 编译单元：ClassName.java 每个.java文件内最多能有一个public类，且该类必须与文件同名 文件内其他类在包内可见，仅用来为主public类提供支持 .java文件经编译生成对应的.class文件 类库单元：package 包名须使用小写字母 包（类库）实质是一组类文件 package须出现在文件起始处 import可使包内名称在包外可用 默认包：若.java文件未明确设定包名称，则其默认隶属与其目录的默认包中 文件组织 创建一个独一无二的包名：domain.package 查找隐藏于目录结构中的某处的类：将包名分解为机器上的一个目录，并将属于该包的所有.class文件置于该目录下 访问权限 # Java 中有四种访问权限：private、default（默认包访问权限package）、protected 以及 public。\n可以对类或类中的成员（字段和方法）加上访问修饰符。\n类可见表示其它类可以用这个类创建实例对象。 成员可见表示其它类可以用这个类的实例对象访问到该成员； protected 用于修饰成员，表示在继承体系中成员对于子类可见，但是这个访问修饰符对于类没有意义。\n设计良好的模块会隐藏所有的实现细节，把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们的 API 进行通信，一个模块不需要知道其他模块的内部工作情况，这个概念被称为信息隐藏或封装。因此访问权限应当尽可能地使每个类或者成员不被外界访问。\n如果子类的方法重写了父类的方法，那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例去代替，也就是确保满足里氏替换原则。\n字段决不能是公有的，因为这么做的话就失去了对这个字段修改行为的控制，客户端可以对其随意修改。例如下面的例子中，AccessExample 拥有 id 公有字段，如果在某个时刻，我们想要使用 int 存储 id 字段，那么就需要修改所有的客户端代码。\npublic class AccessExample { public String id; } 可以使用公有的 getter 和 setter 方法来替换公有字段，这样的话就可以控制对字段的修改行为。\npublic class AccessExample { private int id; public String getId() { return id + \u0026#34;\u0026#34;; } public void setId(String id) { this.id = Integer.valueOf(id); } } 但是也有例外，如果是包级私有的类或者私有的嵌套类，那么直接暴露成员不会有特别大的影响。\npublic class AccessWithInnerClassExample { private class InnerClass { int x; } private InnerClass innerClass; public AccessWithInnerClassExample() { innerClass = new InnerClass(); } public int getValue() { return innerClass.x; // 直接访问 } } 抽象类与接口 # 1. 抽象类 # 抽象类和抽象方法都使用 abstract 关键字进行声明 抽象方法仅有声明，不能有方法体，抽象方法必须在非抽象的子类中覆盖（具体实现） 抽象类中可以没有任何抽象方法，若一个类中包含抽象方法，那么这个类必须声明为抽象类 抽象类不能被实例化，只能被继承 public abstract class AbstractClassExample { protected int x; private int y; public abstract void func1(); public void func2() { System.out.println(\u0026#34;func2\u0026#34;); } } public class AbstractExtendClassExample extends AbstractClassExample { @Override public void func1() { System.out.println(\u0026#34;func1\u0026#34;); } } // AbstractClassExample ac1 = new AbstractClassExample(); // \u0026#39;AbstractClassExample\u0026#39; is abstract; cannot be instantiated AbstractClassExample ac2 = new AbstractExtendClassExample(); ac2.func1(); 2. 接口 # 接口本身默认为包权限，可声明为public 接口的成员（方法、域/字段）默认都是 public 的，并且不允许定义为 private 或者 protected 接口的域/字段默认都是 static 和 final 的 接口的内部类默认是 static（嵌套类）的，且是可继承的 接口的方法默认是abstract的，不能有方法体 用default声明的接口方法可以有默认方法体 接口是抽象类的延伸，从 Java 8 开始，接口也可以拥有默认的方法实现，这是因为不支持默认方法的接口的维护成本太高了（在 Java 8 之前，它可以看成是一个完全抽象的类，也就是说它不能有任何的方法实现，如果一个接口想要添加新的方法，那么要修改所有实现了该接口的类，让它们都实现新增的方法。）\n// 成员变量只能是 public static final 的 public static final String name = \u0026#34;张三\u0026#34;; String name = \u0026#34;张三\u0026#34;; // 方法总是 public abstract 的 public abstract List\u0026lt;String\u0026gt; getUserNames(Long companyId); List\u0026lt;String\u0026gt; getUserNames(Long companyId); public interface InterfaceExample { void func1(); default void func2(){ System.out.println(\u0026#34;func2\u0026#34;); } int x = 123; // int y; // Variable \u0026#39;y\u0026#39; might not have been initialized public int z = 0; // Modifier \u0026#39;public\u0026#39; is redundant for interface fields // private int k = 0; // Modifier \u0026#39;private\u0026#39; not allowed here // protected int l = 0; // Modifier \u0026#39;protected\u0026#39; not allowed here // private void fun3(); // Modifier \u0026#39;private\u0026#39; not allowed here } public class InterfaceImplementExample implements InterfaceExample { @Override public void func1() { System.out.println(\u0026#34;func1\u0026#34;); } } // InterfaceExample ie1 = new InterfaceExample(); // \u0026#39;InterfaceExample\u0026#39; is abstract; cannot be instantiated InterfaceExample ie2 = new InterfaceImplementExample(); ie2.func1(); System.out.println(InterfaceExample.x); 3. 比较 # 从设计层面上看，抽象类提供了一种 IS-A 关系，需要满足里式替换原则，即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系，它只是提供一种方法实现契约，并不要求接口和实现接口的类具有 IS-A 关系。 从使用上来看，一个类可以实现多个接口，但是不能继承多个抽象类。 接口的字段只能是 static 和 final 类型的，而抽象类的字段没有这种限制。 接口的成员只能是 public 的，而抽象类的成员可以有多种访问权限。 4. 使用选择 # 使用接口：\n需要让不相关的类都实现一个方法，例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法； 需要使用多重继承。 使用抽象类：\n需要在几个相关的类中共享代码。 需要能控制继承来的成员的访问权限，而不是都为 public。 需要继承非静态和非常量字段。 在很多情况下，接口优先于抽象类。因为接口没有抽象类严格的类层次结构要求，可以灵活地为一个类添加行为。并且从 Java 8 开始，接口也可以有默认的方法实现，使得修改接口的成本也变的很低。\nAbstract Methods and Classes 深入理解 abstract class 和 interface When to Use Abstract Class and Interface 内部类（nested class） # 嵌套类（静态内部类）、嵌套接口、多重继承、闭包、回调 （外部类$内部类.class）\n非静态内部类（成员内部类） # 非静态内部类必须寄存在一个外部类对象里。因此，如果有一个非静态内部类对象那么一定存在对应的外部类对象。非静态内部类对象单独属于外部类的某个对象。\n编译为 Outer$Inner.class\n非静态内部类 的 非静态实例 隐式包含对外部类的引用this$0，使用Outer.this访问外部类对象\npublic class Main { public static void main(String[] args) { Outer outer = new Outer(\u0026#34;Nested\u0026#34;); // 实例化一个Outer Outer.Inner inner = outer.new Inner(); // 实例化一个Inner inner.hello(); } } class Outer { private String name; Outer(String name) { this.name = name; } class Inner { void hello() { name += \u0026#34;, call from inner!\u0026#34;; // 可访问外部类私有域 System.out.println(\u0026#34;Hello, \u0026#34; + Outer.this.name); // 可访问外部类的实例 } } } 非静态内部类可以直接访问外部类的成员，但是外部类不能直接访问非静态内部类成员。\n非静态内部类不能有静态方法、静态属性和静态初始化块。\n外部类的静态方法、静态代码块不能访问非静态内部类，包括不能使用非静态内部类定义变量、创建实例。\n静态内部类 # 用static修饰的内部类和Inner Class有很大的不同，它不再依附于Outer的实例，而是一个完全独立的类，因此无法引用Outer.this，但它可以访问Outer的private静态字段和静态方法。如果把StaticNested移到Outer之外，就失去了访问private的权限。编译为 Outer$Inner.class\npublic class Main { public static void main(String[] args) { Outer.StaticNested sn = new Outer.StaticNested(); sn.hello(); } } class Outer { private static String NAME = \u0026#34;OUTER\u0026#34;; private String name; Outer(String name) { this.name = name; } static class StaticNested { void hello() { System.out.println(\u0026#34;Hello, \u0026#34; + Outer.NAME); } } } 匿名内部类 # ​\t适合那种只需要使用一次的类，匿名类被编译为Outer$1.class\npublic class Main { public static void main(String[] args) { Outer outer = new Outer(\u0026#34;Nested\u0026#34;); outer.asyncHello(); } } class Outer { private String name; Outer(String name) { this.name = name; } void asyncHello() { Runnable r = new Runnable() { @Override public void run() { System.out.println(\u0026#34;Hello, \u0026#34; + Outer.this.name); // 匿名内部类引用外部类实例 } }; new Thread(r).start(); } } 局部内部类 # ​\t定义在方法内部的，作用域只限于本方法，称为局部内部类。编译为 Outer$1Inner.class\npackage com.qf.demo1; /* * 局部内部类 * 1.相当于方法里的局部变量，只能在方法中使用 * */ public class Test { public static void main(String[] args) { Test();//局部内部类是随着方法的调用而被执行 } public static void Test() { int a =4; //局部内部类 //inner 局部内部类 不能添加访问权限修饰符 class Inner { private int age; private String name; public void eat() { System.out.print(\u0026#34;吃\u0026#34;); } } //局部内部类只能在声明这个内部类的方法中创建对象 Inner inner = new Inner(); inner.eat(); System.out.println(inner.name); } } super # 访问父类的构造函数：可以使用 super() 函数访问父类的构造函数，从而委托父类完成一些初始化的工作。应该注意到，子类一定会调用父类的构造函数来完成初始化工作，一般是调用父类的默认构造函数，如果子类需要调用父类其它构造函数，那么就可以使用 super() 函数。 访问父类的成员：如果子类重写了父类的某个方法，可以通过使用 super 关键字来引用父类的方法实现。 public class SuperExample { protected int x; protected int y; public SuperExample(int x, int y) { this.x = x; this.y = y; } public void func() { System.out.println(\u0026#34;SuperExample.func()\u0026#34;); } } public class SuperExtendExample extends SuperExample { private int z; public SuperExtendExample(int x, int y, int z) { super(x, y); this.z = z; } @Override public void func() { super.func(); System.out.println(\u0026#34;SuperExtendExample.func()\u0026#34;); } } SuperExample e = new SuperExtendExample(1, 2, 3); e.func(); SuperExample.func() SuperExtendExample.func() Using the Keyword super\n重写与重载 # 1. 重写（Override）\n存在于继承体系中，指子类实现了一个与父类在方法声明上完全相同的一个方法。\n为了满足里式替换原则，重写需要符合下面的三个要点：\n“==”： 方法名、形参列表相同（方法签名相同）。\n“≤”：返回值类型和声明异常类型，子类小于等于父类。\n“≥”： 访问权限，子类大于等于父类。\n使用 @Override 注解，可以让编译器帮忙检查是否满足上面的三个限制条件。\n下面的示例中，SubClass 为 SuperClass 的子类，SubClass 重写了 SuperClass 的 func() 方法。其中：\n子类方法访问权限为 public，大于父类的 protected。 子类的返回类型为 ArrayList，是父类返回类型 List 的子类。 子类抛出的异常类型为 Exception，是父类抛出异常 Throwable 的子类。 子类重写方法使用 @Override 注解，从而让编译器自动检查是否满足限制条件。 class SuperClass { protected List\u0026lt;Integer\u0026gt; func() throws Throwable { return new ArrayList\u0026lt;\u0026gt;(); } } class SubClass extends SuperClass { @Override public ArrayList\u0026lt;Integer\u0026gt; func() throws Exception { return new ArrayList\u0026lt;\u0026gt;(); } } 在调用一个方法时，先从本类中查找看是否有对应的方法，如果没有再到父类中查看，看是否从父类继承来。否则就要对参数进行转型，转成父类之后看是否有对应的方法。总的来说，方法调用的优先级为：\nthis.func(this) super.func(this) this.func(super) super.func(super) /* A | B | C | D */ class A { public void show(A obj) { System.out.println(\u0026#34;A.show(A)\u0026#34;); } public void show(C obj) { System.out.println(\u0026#34;A.show(C)\u0026#34;); } } class B extends A { @Override public void show(A obj) { System.out.println(\u0026#34;B.show(A)\u0026#34;); } } class C extends B { } class D extends C { } public static void main(String[] args) { A a = new A(); B b = new B(); C c = new C(); D d = new D(); // 在 A 中存在 show(A obj)，直接调用 a.show(a); // A.show(A) // 在 A 中不存在 show(B obj)，将 B 转型成其父类 A a.show(b); // A.show(A) // 在 B 中存在从 A 继承来的 show(C obj)，直接调用 b.show(c); // A.show(C) // 在 B 中不存在 show(D obj)，但是存在从 A 继承来的 show(C obj)，将 D 转型成其父类 C b.show(d); // A.show(C) // 引用的还是 B 对象，所以 ba 和 b 的调用结果一样 A ba = new B(); ba.show(c); // A.show(C) ba.show(d); // A.show(C) } 2. 重载（Overload）\n存在于同一个类中，指一个方法与已经存在的方法名称上相同，但是参数类型、个数、顺序至少有一个不同。应该注意的是，返回值不同，其它都相同不算是重载。\n七、反射 # Class对象 # 运行时类型信息的表示\n每个类都有一个Class对象，包含了与类有关的信息。当编译一个新类时，会产生一个同名的 .class 文件，该文件内容保存着 Class 对象。\n类加载相当于 Class 对象的加载，类在第一次使用时（程序创建第一个对类的静态成员的引用时）才动态加载到 JVM 中。也可以使用 Class.forName(\u0026quot;com.mysql.jdbc.Driver\u0026quot;) 这种方式来控制类的加载，该方法会返回一个 Class 对象。\n加载：类加载器查找字节码（.class文件），读取并创建一个Class对象（注意：访问编译期常量、类字面常量不会引起类链接，即惰性初始化） 链接：验证字节码，为静态域分配存储空间 初始化：若有父类，对其初始化；执行静态初始化器与静态初始化块 RTTI与反射 # 运行时识别类和对象信息：\nRTTI（Run-Time Type Identification）：编译器编译时打开和检查.class文件，编译时已经知道所有的类型，可以查询引用所指对象的确切类型 (type)：传统的类型转换，由RTTI确保正确性，若错误，抛出ClassCastException异常 Class对象：包含对象的类型信息 instanceof：查看对象是否是某特定类的实例 反射：.class文件编译时不可获取，运行时打开和检查.calss文件，可以提供运行时的类信息。 Class 和 java.lang.reflect 一起对反射提供了支持，java.lang.reflect 类库主要包含了以下三个类：\nField ：class.getField()，可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段； Method ：class.getMethod()，可以使用 invoke() 方法调用与 Method 对象关联的方法； Constructor ：class.Constructor()，可以用 Constructor 的 newInstance() 创建新的对象。 反射的优点：\n可扩展性：应用程序可以利用全限定名创建可扩展对象的实例，来使用来自外部的用户自定义类。 类浏览器和可视化开发环境：一个类浏览器需要可以枚举类的成员。可视化开发环境（如 IDE）可以从利用反射中可用的类型信息中受益，以帮助程序员编写正确的代码。 调试器和测试工具： 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义，以确保一组测试中有较高的代码覆盖率。 反射的缺点：\n尽管反射非常强大，但也不能滥用。如果一个功能可以不用反射完成，那么最好就不用。在我们使用反射技术时，下面几条内容应该牢记于心。\n性能开销：反射涉及了动态类型的解析，所以 JVM 无法对这些代码进行优化。因此，反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。\n安全限制：使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行，如 Applet，那么这就是个问题了。\n内部暴露：由于反射允许代码执行一些在正常情况下不被允许的操作（比如访问私有的属性和方法），所以使用反射可能会导致意料之外的副作用，这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性，因此当平台发生改变的时候，代码的行为就有可能也随着变化。\nTrail: The Reflection API 深入解析 Java 反射（1）- 基础 八、异常 # Throwable 可以用来表示任何可以作为异常抛出的类，分为两种： Error 和 Exception。其中 Error 用来表示 JVM 无法处理的错误，Exception 分为两种：\n受检异常 ：需要用 try\u0026hellip;catch\u0026hellip; 语句捕获并进行处理，并且可以从异常中恢复； 非受检异常 ：是程序运行时错误（RuntimeException），例如除 0 会引发 Arithmetic Exception，此时程序崩溃并且无法恢复。 Java 入门之异常处理 Java 异常的面试问题及答案 -Part 1 九、泛型 # 泛型（generics）字面意义为泛化的类型，即参数化类型。\n泛型类 # 泛型的类型参数只能是类类型，不能是简单类型。 不能对确切的泛型类型使用instanceof操作。 //此处T可以随便写为任意标识，常见的如T、E、K、V等形式的参数常用于表示泛型 //在实例化泛型类时，必须指定T的具体类型 public class Box\u0026lt;T\u0026gt; { // key这个成员变量的类型为T，T的类型在实例化时指定 private T key; public void set(T key) { this.key = key; } public T get() { return key; } } 泛型接口 # 泛型接口与泛型类的定义及使用基本相同，常被用在各种类的生成器中\n//定义一个泛型接口 public interface Generator\u0026lt;T\u0026gt; { public T next(); } /**当实现泛型接口的类，未传入泛型实参时，在声明类的时候需将泛型的声明也一起加到类中： * 即：class FruitGenerator\u0026lt;T\u0026gt; implements Generator\u0026lt;T\u0026gt;{ * 如果不声明泛型，如：class FruitGenerator implements Generator\u0026lt;T\u0026gt;，编译器会报错：\u0026#34;Unknown class\u0026#34; */ class FruitGenerator\u0026lt;T\u0026gt; implements Generator\u0026lt;T\u0026gt;{ @Override public T next() { return null; } } /**当实现泛型接口的类，传入泛型实参时，则所有使用泛型的地方都要替换成传入的实参类型 * 即：Generator\u0026lt;T\u0026gt;，public T next();中的的T都要替换成传入的String类型。 */ public class FruitGenerator implements Generator\u0026lt;String\u0026gt; { private String[] fruits = new String[]{\u0026#34;Apple\u0026#34;, \u0026#34;Banana\u0026#34;, \u0026#34;Pear\u0026#34;}; @Override public String next() { Random rand = new Random(); return fruits[rand.nextInt(3)]; } } 泛型方法 # 定义泛型方法的规则如下：\n所有泛型方法声明都有一个类型参数声明部分（由尖括号分隔），该类型参数声明部分在方法返回类型之前（在下面例子中的 ）。 每一个类型参数声明部分包含一个或多个类型参数，参数间用逗号隔开。一个泛型参数，也被称为一个类型变量，是用于指定一个泛型类型名称的标识符。 类型参数能被用来声明返回值类型，并且能作为泛型方法得到的实际参数类型的占位符。 泛型方法体的声明和其他方法一样。注意类型参数 只能代表引用型类型，不能是原始类型 （像 int,double,char 的等）。 public class GenericMethodTest { // 泛型方法 printArray public static \u0026lt; E \u0026gt; void printArray( E[] inputArray ){ // 输出数组元素 for ( E element : inputArray ){ System.out.printf( \u0026#34;%s \u0026#34;, element ); } System.out.println(); } /*泛型方法与可变参数 * args实质是T[]类型的数组 */ public \u0026lt;T\u0026gt; void printMsg( T... args){ for(T t : args){ System.out.println(\u0026#34;泛型测试\u0026#34; + \u0026#34;t is \u0026#34; + t.getClass() + \u0026#34;\\t\u0026#34; + t); } } public static void main( String args[] ){ // 创建不同类型数组： Integer, Double 和 Character Integer[] intArray = { 1, 2, 3, 4, 5 }; Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 }; Character[] charArray = { \u0026#39;H\u0026#39;, \u0026#39;E\u0026#39;, \u0026#39;L\u0026#39;, \u0026#39;L\u0026#39;, \u0026#39;O\u0026#39;}; System.out.println( \u0026#34;整型数组元素为:\u0026#34; ); printArray( intArray ); // 传递一个整型\tSystem.out.println( \u0026#34;\\n双精度型数组元素为:\u0026#34; ); printArray( doubleArray ); // 传递一个双精度型\tSystem.out.println( \u0026#34;\\n字符型数组元素为:\u0026#34; ); printArray( charArray ); // 传递一个字符型数组 printMsg(\u0026#34;111\u0026#34;,222,\u0026#34;aaaa\u0026#34;,\u0026#34;2323.4\u0026#34;,55.55); // 实际传入java.io.Serializable类型数组 } } 泛型数组 # 在java中不能创建一个确切的泛型类型的数组，使用通配符创建泛型数组是可以的。\n// 不能创建一个确切的泛型类型的数组 // List\u0026lt;String\u0026gt;[] ls = new ArrayList\u0026lt;String\u0026gt;[10]; // 使用通配符创建泛型数组是可以的 List\u0026lt;?\u0026gt;[] ls = new ArrayList\u0026lt;?\u0026gt;[10]; // raw use 也是可以的 List\u0026lt;String\u0026gt;[] ls = new ArrayList[10]; // 这种有类型检查遗漏风险问题 List[] ls = new ArrayList[10]; public class GenericTest { public static void main(String[] args) throws Exception { List\u0026lt;String\u0026gt;[] ls = new List[2]; List\u0026lt;Object\u0026gt; l = new LinkedList\u0026lt;\u0026gt;(); l.add(new Object()); //\tls[0] = l; // 类型检查不通过 // 绕过了类型检查 Object[] obgList = (Object[])ls; obgList[0] = l; obgList[1] = new ArrayList\u0026lt;Integer\u0026gt;(); /* * class [Ljava.util.List; * 泛型测试t is class java.util.LinkedList\t[java.lang.Object@2a84aee7] * 泛型测试t is class java.util.ArrayList\t[] */ printMsg(obgList); } } 泛型通配符与上下界 # 类型通配符一般是使用 ? 代替具体的类型参数。例如 List\u0026lt;?\u0026gt; 在逻辑上是 List\u0026lt;String\u0026gt;，List\u0026lt;Integer\u0026gt; 等所有 List\u0026lt;具体类型实参\u0026gt; 的父类。 类型通配符上限通过形如 List\u0026lt;? extends Number\u0026gt; 来定义，如此定义就是通配符泛型值接受 Number 及其下层子类类型。 类型通配符下限通过形如 List\u0026lt;? super Number\u0026gt; 来定义，表示类型只能接受 Number 及其父类类型，如 Objec 类型的实例。 public class GenericsAndCovariance { public static void main(String[] args) { /* 限定上界的不允许写入 */ List\u0026lt;? extends Fruit\u0026gt; flist = new ArrayList\u0026lt;Apple\u0026gt;(); // Compile Error: can’t add any type of object: // flist.add(new Apple()); // flist.add(new Fruit()); // flist.add(new Object()); flist.add(null); // Legal but uninteresting // We know that it returns at least Fruit: Fruit f = flist.get(0); } /* 限定下界的可写入下届及其子类 */ static void writeTo(List\u0026lt;? super Apple\u0026gt; apples) { apples.add(new Apple()); apples.add(new Jonathan()); // apples.add(new Fruit()); // Error } } 泛型擦除 # **泛型只在编译阶段有效，泛型类型在逻辑上看以看成是多个不同的类型，实际上都是相同的基本类型。***在泛型代码内部，无法获得任何有关泛型参数类型的信息*\n通过上面的例子可以证明，在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型，只在编译阶段有效。在编译过程中，正确检验泛型结果后，会将泛型的相关信息擦出，并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说，泛型信息不会进入到运行时阶段。\npublic class ErasedTypeEquivalence { public static void main(String[] args) { Class c1 = new ArrayList\u0026lt;String\u0026gt;().getClass(); Class c2 = new ArrayList\u0026lt;Integer\u0026gt;().getClass(); System.out.println(c1 == c2); // true } } // 在泛型代码内部，无法获得任何有关泛型参数类型的信息，运行时参数T实质为Object类型 // 利用java的RTTI运行时类型信息与反射，可以解决这个问题，不过使用时要显式传入Class参数 public class HasF { public void f() { System.out.println(\u0026#34;HasF.f()\u0026#34;); } } class Manipulator\u0026lt;T\u0026gt; { private T obj; public Manipulator(T x) { obj = x; } // Error: cannot find symbol: method f(): public void manipulate() { obj.f(); } } public class Manipulation { public static void main(String[] args) { HasF hf = new HasF(); Manipulator\u0026lt;HasF\u0026gt; manipulator = new Manipulator\u0026lt;HasF\u0026gt;(hf); manipulator.manipulate(); } } Java 泛型详解 10 道 Java 泛型面试题 十、注解 # Java 注解是附加在代码中的一些元信息，用于一些工具在编译、运行时进行解析和使用，起到说明、配置的功能。 注解不会也不能影响代码的实际逻辑，仅仅起到辅助性的作用。编译器对其生成与不带注解的代码相同的虚拟机指令。 用法 # 编译检查：@SuppressWarnings, @Deprecated与@Override 在反射中使用Annotation：测试、日志、事务等代码自动生成 附属文件自动生成：@Documented 语法 # 声明用法：包、类、接口 | 方法、成员、局部变量、参数变量、类型参数\n类型用法java\n惯用法：注意注解位置应由注解接口指定\nprivate @NonNull String text; // Annotates the type use @Id private String userId; // Annotates the variable public User getUser(@NonNull String userId) // userId 被注解了，同时其参数类型为 @NonNull String 每一个注解必须通过一个注解接口进行定义\n@Retention(RetentionPolicy.RUNTIME) public @interface BugReport { // 继承自java.lang.annotation.Annotation enum Status {UNCONFIRMED, CONFIRMED, FIXED, NOTABUG}; boolean showStopper() default false; String assignedTo() default \u0026#34;[none]\u0026#34;; Class\u0026lt;?\u0026gt; testCase() default Void.class; Status status() default Status.UNCONFIRMED; // 参数由编译器计算而来，默认值应为编译期常量，且不为null Reference ref() default @Reference(); // an annotation type，注意不要引入循环依赖 String[] reportedBy(); // 数组赋值需要加{}: `reportBy={\u0026#34;charles\u0026#34;, \u0026#34;Cheung\u0026#34;}` } 标记注解 @Test 适用于注解无元素或者都有默认值 单值注解 @SingleValue(\u0026quot;name\u0026quot;) 适用于仅有一个元素的注解 元注解 # 元注解 作用 Target 描述注解的使用范围（即被修饰的注解可以用在什么地方） Reteniton 描述注解保留的时间范围（即：被描述的注解在它所修饰的类中可以被保留到何时） Documented 描述在使用 javadoc 工具为类生成帮助文档时是否要保留其注解信息。 Inherited 使被它修饰的注解具有继承性（如果某个类使用了被@Inherited修饰的注解，则其子类将自动具有该注解） Repeatable 允许在同一申明类型（类，属性，或方法）前多次使用同一个类型注解 public enum ElementType { TYPE, // 类、接口、枚举类 FIELD, // 成员变量（包括：枚举常量） METHOD, // 成员方法 PARAMETER, // 方法参数 CONSTRUCTOR, // 构造方法 LOCAL_VARIABLE, // 局部变量 ANNOTATION_TYPE, // 注解类 PACKAGE, // 可用于修饰：包 TYPE_PARAMETER, // 变量注解：类型参数，JDK 1.8 新增，表示该注解能写在类型参数的声明语句中 TYPE_USE // 类型注解：使用类型的任何地方，JDK 1.8 新增 } public enum RetentionPolicy { SOURCE, // 源文件保留,如 @Override 和 @SuppressWarnings，不包含在class文件中 CLASS, // 编译期保留，默认值，包含在class文件中，但不加载进虚拟机 RUNTIME // 运行期保留，可通过反射去获取注解信息 } 标准注解 # 注解 作用域 作用 Deprecated 方法 对已过时的方法发出警告 SuppressedWarnings 阻止特定类型的警告信息 Override 方法 检查方法是否真正覆盖超类的方法 Generated 所有 代码生成使用，用以区分与程序员编写代码 PostConstruct 方法 控制对象生命周期：对象构建后调用 PreDestroy 方法 控制对象生命周期：对象销毁前调用 Resource 类、接口、方法、域 资源注入 字节码工程 # asm 修改类文件 # 加载时修改字节码 # 注解 Annotation 实现原理与自定义注解例子\nJava Annotation认知(包括框架图、详细介绍、示例说明)\n十一、Lambda表达式与Stream API # 编程思想：\n面向过程编程 面向对象编程 函数式编程 面向切面编程 面向消息编程 1. Lambda 表达式 # Lambda 表达式是一个匿名函数，即没有函数名的函数，可以表示闭包。Lambda 表达式简化了匿名内部类的形式，但其实内部的实现原理却不相同：匿名内部类在编译之后会创建一个新的匿名内部类出来，而 Lambda 被编译器封装为主类的一个private static 方法，然后调用 JVM invokedynamic指令实现的，并不会产生新类。其使用有两个条件：\n必须有相应的函数接口（函数接口是指内部只有一个抽象方法的接口，通常使用@FunctionalInterface标注） 类型推断机制：在上下文信息足够的情况下，编译器可以推断出参数表的类型，而不需要显式指名 2. Lambda 写法 # Runnable run = () -\u0026gt; System.out.println(\u0026#34;Hello World\u0026#34;);// 1 无参数 ActionListener listener = event -\u0026gt; System.out.println(\u0026#34;button clicked\u0026#34;);// 2 单参数 BinaryOperator\u0026lt;Long\u0026gt; add = (Long x, Long y) -\u0026gt; x + y;// 3 多参数 BinaryOperator\u0026lt;Long\u0026gt; addImplicit = (x, y) -\u0026gt; x + y;// 4 多参数的类型推断 Runnable multiLine = () -\u0026gt; {// 5 代码块 System.out.print(\u0026#34;Hello\u0026#34;); System.out.println(\u0026#34; Hoolee\u0026#34;); }; // 与匿名内部类对比 new Thread(new Runnable(){// 接口名 @Override public void run(){// 方法名 System.out.println(\u0026#34;Thread run()\u0026#34;); } }).start(); new Thread( () -\u0026gt; System.out.println(\u0026#34;Thread run()\u0026#34;) // 省略接口名和方法名 ).start(); 3. 方法引用与构造器引用 # 方法引用可以将一个方法赋给一个变量或者作为参数传递给另外一个方法，甚至将方法作为一个函数式接口的实例。\n诸如String::length的语法形式叫做方法引用（method references），这种语法用来替代某些特定形式Lambda表达式。如果Lambda表达式的全部内容就是调用一个已有的方法，那么可以用方法引用来替代Lambda表达式。方法引用可以细分为四类：\n方法引用类别 举例 引用静态方法 Integer::sum 引用某个对象的方法 list::add 引用某个类的方法 String::length 引用构造方法 HashMap::new // 引用方法的参数个数、类型，返回值类型要和函数式接口中的方法声明一一对应才行 Comparator\u0026lt;Integer\u0026gt; comparator = Integer::compare; int result = comparator.compare(100,10); IntBinaryOperator intBinaryOperator = Integer::compare; int result = intBinaryOperator.applyAsInt(10,100); // 构造器引用 Integer::new 4. Stream # BaseStream接口包括四个继承接口，其中IntStream, LongStream, DoubleStream对应三种基本类型（int, long, double，注意不是包装类型），Stream对应所有剩余类型的stream视图。为不同数据类型设置不同stream接口，可以\n提高性能 增加特定接口函数。 大部分情况下stream是容器调用Collection.stream()方法得到的，但stream和collections有以下不同：\n无存储。stream不是一种数据结构，它只是某种数据源的一个视图，数据源可以是一个数组，Java容器或I/O channel等。 为函数式编程而生。对stream的任何修改都不会修改背后的数据源，比如对stream执行过滤操作并不会删除被过滤的元素，而是会产生一个不包含被过滤元素的新stream。 惰式执行。stream上的操作并不会立即执行，只有等到用户真正需要结果的时候才会执行。 可消费性。stream只能被“消费”一次，一旦遍历过就会失效，就像容器的迭代器那样，想要再次遍历必须重新生成。 5. Stream API # 对stream的操作分为两类，可以通过方法的返回值进行区分，即返回值为stream的大都是中间操作，否则是结束操作。\n中间操作（intermediate operations）：总是会惰式执行，调用中间操作只会生成一个标记了该操作的新stream，仅此而已。 结束操作（terminal operations）：会触发实际计算，计算发生时会把所有中间操作积攒的操作以pipeline的方式执行，这样可以减少迭代次数。计算完成之后stream就会失效。 操作类型 接口方法 中间操作 concat() distinct() filter() flatMap() limit() map() peek() skip() sorted() parallel() sequential() unordered() 结束操作 allMatch() anyMatch() collect() count() findAny() findFirst() forEach() forEachOrdered() max() min() noneMatch() reduce() toArray() 规约操作（reduction operation）又被称作折叠操作（fold），是通过某个连接动作将所有元素汇总成一个汇总结果的过程。元素求和、求最大值或最小值、求出元素总个数、将所有元素转换成一个列表或集合，都属于规约操作。Stream类库有两个通用的规约操作reduce()和collect()\n6. Reduce详解 # reduce操作可以实现从一组元素中生成一个值，sum()、max()、min()、count()等都是reduce操作，将他们单独设为函数只是因为常用。reduce()的方法定义有三种重写形式：\nOptional\u0026lt;T\u0026gt; reduce(BinaryOperator\u0026lt;T\u0026gt; accumulator) T reduce(T identity, BinaryOperator\u0026lt;T\u0026gt; accumulator) \u0026lt;U\u0026gt; U reduce(U identity, BiFunction\u0026lt;U,? super T,U\u0026gt; accumulator, BinaryOperator\u0026lt;U\u0026gt; combiner) 虽然函数定义越来越长，但语义不曾改变，多的参数只是为了指明初始值（参数identity），或者是指定并行执行时多个部分结果的合并方式（参数combiner）。reduce()最常用的场景就是从一堆值中生成一个值。用这么复杂的函数去求一个最大或最小值，你是不是觉得设计者有病。其实不然，因为“大”和“小”或者“求和”有时会有不同的语义。\n// 求单词长度之和 Stream\u0026lt;String\u0026gt; stream = Stream.of(\u0026#34;I\u0026#34;, \u0026#34;love\u0026#34;, \u0026#34;you\u0026#34;, \u0026#34;too\u0026#34;); Integer lengthSum = stream.reduce(0,　// 初始值　// (1) (sum, str) -\u0026gt; sum+str.length(), // 累加器 // (2) (a, b) -\u0026gt; a+b);　// 部分和拼接器，并行执行时才会用到 // (3) // int lengthSum = stream.mapToInt(str -\u0026gt; str.length()).sum(); System.out.println(lengthSum); 7. Collect详解 # collect()是Stream接口方法中最灵活的一个，如果某个功能在Stream接口中没找到，十有八九可以通过collect()方法实现，其方法签名为：\n\u0026lt;R\u0026gt; R collect(Supplier\u0026lt;R\u0026gt; supplier, BiConsumer\u0026lt;R,? super T\u0026gt; accumulator, BiConsumer\u0026lt;R,R\u0026gt; combiner) \u0026lt;R,A\u0026gt; R collect(Collector\u0026lt;? super T,A,R\u0026gt; collector) 收集器（Collector）是为Stream.collect()方法量身打造的工具接口（类）。考虑一下将一个Stream转换成一个容器（或者Map）需要做哪些工作？我们至少需要：\n目标容器是什么？是*ArrayList还是HashSet，或者是个TreeMap*。 新元素如何添加到容器中？是List.add()还是Map.put()。 如果并行的进行规约，还需要告诉collect()如何将多个部分结果合并成一个。 通常情况下我们不需要手动指定collect()的三个参数，而是调用collect(Collector\u0026lt;? super T,A,R\u0026gt; collector)方法，并且参数中的Collector对象大都是直接通过Collectors工具类获得。实际上传入的收集器的行为决定了collect()的行为。\nStream\u0026lt;String\u0026gt; stream = Stream.of(\u0026#34;I\u0026#34;, \u0026#34;love\u0026#34;, \u0026#34;you\u0026#34;, \u0026#34;too\u0026#34;); List\u0026lt;String\u0026gt; list = stream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll); // (0) // 使用Collector List\u0026lt;String\u0026gt; list = stream.collect(Collectors.toList()); // (1) Set\u0026lt;String\u0026gt; set = stream.collect(Collectors.toSet()); // (2) // 使用toCollection()指定规约容器的类型 ArrayList\u0026lt;String\u0026gt; arrayList = stream.collect(Collectors.toCollection(ArrayList::new));// (3) HashSet\u0026lt;String\u0026gt; hashSet = stream.collect(Collectors.toCollection(HashSet::new));// (4) Stream背后依赖于某种数据源，数据源可以是数组、容器等，但不能是Map。反过来从*Stream生成Map是可以的，但我们要想清楚Map的key和value分别代表什么，根本原因是我们要想清楚要干什么。通常在三种情况下collect()的结果会是Map*：\n使用Collectors.toMap()生成的收集器，用户需要指定如何生成Map的key和value。 使用Collectors.partitioningBy()生成的收集器，对元素进行二分区操作时用到。 使用Collectors.groupingBy()生成的收集器，对元素做group操作时用到。 // 使用toMap()统计学生GPA Map\u0026lt;Student, Double\u0026gt; studentToGPA = students.stream().collect(Collectors.toMap(Function.identity(),// 如何生成key student -\u0026gt; computeGPA(student)));// 如何生成value // Partition students into passing and failing Map\u0026lt;Boolean, List\u0026lt;Student\u0026gt;\u0026gt; passingFailing = students.stream() .collect(Collectors.partitioningBy(s -\u0026gt; s.getGrade() \u0026gt;= PASS_THRESHOLD)); // Group employees by department Map\u0026lt;Department, List\u0026lt;Employee\u0026gt;\u0026gt; byDept = employees.stream() .collect(Collectors.groupingBy(Employee::getDepartment)); // 根据部门分组 // 使用下游收集器统计每个部门的人数 Map\u0026lt;Department, Integer\u0026gt; totalByDept = employees.stream() .collect(Collectors.groupingBy(Employee::getDepartment, Collectors.counting()));// 下游收集器，count聚合 // 按照部门对员工分布组，并只保留员工的名字 Map\u0026lt;Department, List\u0026lt;String\u0026gt;\u0026gt; byDept = employees.stream() .collect(Collectors.groupingBy(Employee::getDepartment, Collectors.mapping(Employee::getName,// 下游收集器 Collectors.toList())));// 更下游的收集器 字符串拼接时使用Collectors.joining()生成的收集器，从此告别for循环。Collectors.joining()方法有三种重写形式，分别对应三种不同的拼接方式。\n// 使用Collectors.joining()拼接字符串 Stream\u0026lt;String\u0026gt; stream = Stream.of(\u0026#34;I\u0026#34;, \u0026#34;love\u0026#34;, \u0026#34;you\u0026#34;); //String joined = stream.collect(Collectors.joining()); // \u0026#34;Iloveyou\u0026#34; //String joined = stream.collect(Collectors.joining(\u0026#34;,\u0026#34;)); // \u0026#34;I,love,you\u0026#34; String joined = stream.collect(Collectors.joining(\u0026#34;,\u0026#34;, \u0026#34;{\u0026#34;, \u0026#34;}\u0026#34;)); // \u0026#34;{I,love,you}\u0026#34; String joined = stream.collect(StringBuilder::new, (s1, s2) -\u0026gt; { // \u0026#34;{I,love,you}\u0026#34; if (StringUtils.isEmpty(s1)) { s1.append(s2); } else { s1.append(\u0026#39;,\u0026#39;).append(s2); } }, StringBuilder::append).append(\u0026#39;}\u0026#39;).insert(0, \u0026#39;{\u0026#39;).toString(); 8. Stream Pipelines原理 # 直观的讲，为每一次函数调用都执一次迭代一定能够实现功能，但效率上肯定是无法接受的：\n迭代次数多。迭代次数跟函数调用的次数相等。 频繁产生中间结果。每次函数调用都产生一次中间结果，存储开销无法接受。 Stream类库的实现着使用流水线（Pipeline）的方式巧妙的避免了多次迭代，其基本思想是在一次迭代中尽可能多的执行用户指定的操作，具体来讲其将操作分为两类：中间操作和结束操作\n中间操作只是一种标记，在遇到结束操作之前只是把中间操作记录了下来。中间操作可以分为无状态的(Stateless)和有状态的(Stateful)： 无状态中间操作是指元素的处理不受前面元素的影响。 有状态的中间操作必须等到所有元素处理之后才知道最终结果，比如排序是有状态操作，在读取所有元素之前并不能确定排序结果； 结束操作会触发实际计算，可以分为短路操作和非短路操作： 短路操作是指不用处理全部元素就可以返回结果，比如找到第一个满足条件的元素。 非短路操作必须处理完所有元素才能返回结果。 Stream操作 分类 中间操作(Intermediate operations) 无状态(Stateless) unordered() filter() map() mapToInt() mapToLong() mapToDouble() flatMap() flatMapToInt() flatMapToLong() flatMapToDouble() peek() 有状态(Stateful) distinct() sorted() sorted() limit() skip() 结束操作(Terminal operations) 短路操作(short-circuiting) anyMatch() allMatch() noneMatch() findFirst() findAny() 非短路操作 forEach() forEachOrdered() toArray() reduce() collect() max() min() count() 之所以要进行如此精细的划分，是因为底层对每一种情况的处理方式不同，具体来讲，有以下几个问题：\n用户的操作如何记录？ 操作如何叠加？ 叠加之后的操作如何执行？ 执行后的结果（如果有）在哪里？ 1. 操作如何记录？ # 注意这里使用的是“操作(operation)”一词，指的是“Stream中间操作”的操作，很多Stream操作会需要一个回调函数（Lambda表达式），因此一个完整的操作是**\u0026lt;数据源，操作，回调函数\u0026gt;**构成的三元组。Stream中使用Stage的概念来描述一个完整的操作，并用某种实例化后的PipelineHelper来代表Stage，将具有先后顺序的各个Stage连到一起，就构成了整个流水线。跟Stream相关类和接口的继承关系图示。\n还有IntPipeline, LongPipeline, DoublePipeline没在图中画出，这三个类专门为三种基本类型（不是包装类型）而定制的，跟ReferencePipeline是并列关系。\n图中Head用于表示第一个Stage，即调用诸如Collection.stream()方法产生的Stage，很显然这个Stage里不包含任何操作；StatelessOp和StatefulOp分别表示无状态和有状态的Stage，对应于无状态和有状态的中间操作。Stream流水线组织结构示意图如下：\n图中通过Collection.stream()方法得到Head也就是stage0，紧接着调用一系列的中间操作，不断产生新的Stream。这些Stream对象以双向链表的形式组织在一起，构成整个流水线，由于每个Stage都记录了前一个Stage和本次的操作以及回调函数，依靠这种结构就能建立起对数据源的所有操作。这就是Stream记录操作的方式。\n2. 操作如何叠加？ # 以上只是解决了操作记录的问题，要想让流水线起到应有的作用我们需要一种将所有操作叠加到一起的方案。你可能会觉得这很简单，只需要从流水线的head开始依次执行每一步的操作（包括回调函数）就行了。这听起来似乎是可行的，但是你忽略了前面的Stage并不知道后面Stage到底执行了哪种操作，以及回调函数是哪种形式。换句话说，只有当前Stage本身才知道该如何执行自己包含的动作。这就需要有某种协议来协调相邻Stage之间的调用关系。\n这种协议由Sink接口完成，Sink接口包含的方法如下表所示：\n方法名 作用 void begin(long size) 开始遍历元素之前调用该方法，通知Sink做好准备。 void end() 所有元素遍历完成之后调用，通知Sink没有更多的元素了。 boolean cancellationRequested() 是否可以结束操作，可以让短路操作尽早结束。 void accept(T t) 遍历元素时调用，接受一个待处理元素，并对元素进行处理。Stage把自己包含的操作和回调方法封装到该方法里，前一个Stage只需要调用当前Stage.accept(T t)方法就行了。 有了上面的协议，相邻Stage之间调用就很方便了，每个Stage都会将自己的操作封装到一个Sink里，前一个Stage只需调用后一个Stage的accept()方法即可，并不需要知道其内部是如何处理的。Sink的四个接口方法常常相互协作，共同完成计算任务。实际上Stream API内部实现的的本质，就是如何重载Sink的这四个接口方法。\n对于有状态的操作，Sink的begin()和end()方法也是必须实现的。比如Stream.sorted()是一个有状态的中间操作，其对应的Sink.begin()方法可能创建一个存放结果的容器，而accept()方法负责将元素添加到该容器，最后end()负责对容器进行排序。 对于短路操作，Sink.cancellationRequested()也是必须实现的，比如Stream.findFirst()是短路操作，只要找到一个元素，cancellationRequested()就应该返回true，以便调用者尽快结束查找。 有了Sink对操作的包装，Stage之间的调用问题就解决了，执行时只需要从流水线的head开始对数据源依次调用每个Stage对应的Sink.{begin(), accept(), cancellationRequested(), end()}方法就可以了。一种可能的Sink.accept()方法流程是这样的：\nvoid accept(U u){ 1. 使用当前Sink包装的回调函数处理u 2. 将处理结果传递给流水线下游的Sink } Sink接口的其他几个方法也是按照这种[处理-\u0026gt;转发]的模型实现。下面我们结合具体例子看看Stream的中间操作是如何将自身的操作包装成Sink以及Sink是如何将处理结果转发给下一个Sink的。先看Stream.map()方法：\n// Stream.map()，调用该方法将产生一个新的Stream public final \u0026lt;R\u0026gt; Stream\u0026lt;R\u0026gt; map(Function\u0026lt;? super P_OUT, ? extends R\u0026gt; mapper) { ... return new StatelessOp\u0026lt;P_OUT, R\u0026gt;(this, StreamShape.REFERENCE, StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) { @Override /*opWripSink()方法返回由回调函数包装而成Sink*/ Sink\u0026lt;P_OUT\u0026gt; opWrapSink(int flags, Sink\u0026lt;R\u0026gt; downstream) { return new Sink.ChainedReference\u0026lt;P_OUT, R\u0026gt;(downstream) { @Override public void accept(P_OUT u) { R r = mapper.apply(u);// 1. 使用当前Sink包装的回调函数mapper处理u downstream.accept(r);// 2. 将处理结果传递给流水线下游的Sink } }; } }; } 上述代码看似复杂，其实逻辑很简单，就是将回调函数mapper包装到一个Sink当中。由于Stream.map()是一个无状态的中间操作，所以map()方法返回了一个StatelessOp内部类对象（一个新的Stream），调用这个新Stream的opWripSink()方法将得到一个包装了当前回调函数的Sink。\n再来看一个复杂一点的例子。Stream.sorted()方法将对Stream中的元素进行排序，显然这是一个有状态的中间操作，因为读取所有元素之前是没法得到最终顺序的。抛开模板代码直接进入问题本质，sorted()方法是如何将操作封装成Sink的呢？sorted()一种可能封装的Sink代码如下：\n// Stream.sort()方法用到的Sink实现 class RefSortingSink\u0026lt;T\u0026gt; extends AbstractRefSortingSink\u0026lt;T\u0026gt; { private ArrayList\u0026lt;T\u0026gt; list;// 存放用于排序的元素 RefSortingSink(Sink\u0026lt;? super T\u0026gt; downstream, Comparator\u0026lt;? super T\u0026gt; comparator) { super(downstream, comparator); } @Override public void begin(long size) { ... // 创建一个存放排序元素的列表 list = (size \u0026gt;= 0) ? new ArrayList\u0026lt;T\u0026gt;((int) size) : new ArrayList\u0026lt;T\u0026gt;(); } @Override public void end() { list.sort(comparator);// 只有元素全部接收之后才能开始排序 downstream.begin(list.size()); if (!cancellationWasRequested) {// 下游Sink不包含短路操作 list.forEach(downstream::accept);// 2. 将处理结果传递给流水线下游的Sink } else {// 下游Sink包含短路操作 for (T t : list) {// 每次都调用cancellationRequested()询问是否可以结束处理。 if (downstream.cancellationRequested()) break; downstream.accept(t);// 2. 将处理结果传递给流水线下游的Sink } } downstream.end(); list = null; } @Override public void accept(T t) { list.add(t);// 1. 使用当前Sink包装动作处理t，只是简单的将元素添加到中间列表当中 } } 上述代码完美的展现了Sink的四个接口方法是如何协同工作的：\n首先beging()方法告诉Sink参与排序的元素个数，方便确定中间结果容器的的大小； 之后通过accept()方法将元素添加到中间结果当中，最终执行时调用者会不断调用该方法，直到遍历所有元素； 最后end()方法告诉Sink所有元素遍历完毕，启动排序步骤，排序完成后将结果传递给下游的Sink； 如果下游的Sink是短路操作，将结果传递给下游时不断询问下游cancellationRequested()是否可以结束处理。 3. 叠加之后的操作如何执行？ # Sink完美封装了Stream每一步操作，并给出了[处理-\u0026gt;转发]的模式来叠加操作。这一连串的齿轮已经咬合，就差最后一步拨动齿轮启动执行。是什么启动这一连串的操作呢？也许你已经想到了启动的原始动力就是结束操作(Terminal Operation)，一旦调用某个结束操作，就会触发整个流水线的执行。\n结束操作之后不能再有别的操作，所以结束操作不会创建新的流水线阶段(Stage)，直观的说就是流水线的链表不会在往后延伸了。结束操作会创建一个包装了自己操作的Sink，这也是流水线中最后一个Sink，这个Sink只需要处理数据而不需要将结果传递给下游的Sink（因为没有下游）。对于Sink的[处理-\u0026gt;转发]模型，结束操作的Sink就是调用链的出口。\n我们再来考察一下上游的Sink是如何找到下游Sink的。一种可选的方案是在PipelineHelper中设置一个Sink字段，在流水线中找到下游Stage并访问Sink字段即可。但Stream类库的设计者没有这么做，而是设置了一个Sink AbstractPipeline.opWrapSink(int flags, Sink downstream)方法来得到Sink，该方法的作用是返回一个新的包含了当前Stage代表的操作以及能够将结果传递给downstream的Sink对象。为什么要产生一个新对象而不是返回一个Sink字段？这是因为使用opWrapSink()可以将当前操作与下游Sink（上文中的downstream参数）结合成新Sink。试想只要从流水线的最后一个Stage开始，不断调用上一个Stage的opWrapSink()方法直到最开始（不包括stage0，因为stage0代表数据源，不包含操作），就可以得到一个代表了流水线上所有操作的Sink，用代码表示就是这样：\n// AbstractPipeline.wrapSink() // 从下游向上游不断包装Sink。如果最初传入的sink代表结束操作， // 函数返回时就可以得到一个代表了流水线上所有操作的Sink。 final \u0026lt;P_IN\u0026gt; Sink\u0026lt;P_IN\u0026gt; wrapSink(Sink\u0026lt;E_OUT\u0026gt; sink) { ... for (AbstractPipeline p=AbstractPipeline.this; p.depth \u0026gt; 0; p=p.previousStage) { sink = p.opWrapSink(p.previousStage.combinedFlags, sink); } return (Sink\u0026lt;P_IN\u0026gt;) sink; } 现在流水线上从开始到结束的所有的操作都被包装到了一个Sink里，执行这个Sink就相当于执行整个流水线，执行Sink的代码如下：\n// AbstractPipeline.copyInto(), 对spliterator代表的数据执行wrappedSink代表的操作。 final \u0026lt;P_IN\u0026gt; void copyInto(Sink\u0026lt;P_IN\u0026gt; wrappedSink, Spliterator\u0026lt;P_IN\u0026gt; spliterator) { ... if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) { wrappedSink.begin(spliterator.getExactSizeIfKnown());// 通知开始遍历 spliterator.forEachRemaining(wrappedSink);// 迭代 wrappedSink.end();// 通知遍历结束 } ... } 上述代码首先调用wrappedSink.begin()方法告诉Sink数据即将到来，然后调用spliterator.forEachRemaining()方法对数据进行迭代（Spliterator是容器的一种迭代器，[参阅](https://github.com/CarpenterLee/JavaLambdaInternals/blob/master/3-Lambda and Collections.md#spliterator)），最后调用wrappedSink.end()方法通知Sink数据处理结束。逻辑如此清晰。\n4. 执行后的结果在哪里？ # 最后一个问题是流水线上所有操作都执行后，用户所需要的结果（如果有）在哪里？首先要说明的是不是所有的Stream结束操作都需要返回结果，有些操作只是为了使用其副作用(Side-effects)，比如使用Stream.forEach()方法将结果打印出来就是常见的使用副作用的场景（事实上，除了打印之外其他场景都应避免使用副作用），对于真正需要返回结果的结束操作结果存在哪里呢？\n特别说明：副作用不应该被滥用，也许你会觉得在Stream.forEach()里进行元素收集是个不错的选择，就像下面代码中那样，但遗憾的是这样使用的正确性和效率都无法保证，因为Stream可能会并行执行。大多数使用副作用的地方都可以使用归约操作更安全和有效的完成。\n// 错误的收集方式 ArrayList\u0026lt;String\u0026gt; results = new ArrayList\u0026lt;\u0026gt;(); stream.filter(s -\u0026gt; pattern.matcher(s).matches()) .forEach(s -\u0026gt; results.add(s)); // Unnecessary use of side-effects! // 正确的收集方式 List\u0026lt;String\u0026gt;results = stream.filter(s -\u0026gt; pattern.matcher(s).matches()) .collect(Collectors.toList()); // No side-effects! 回到流水线执行结果的问题上来，需要返回结果的流水线结果存在哪里呢？这要分不同的情况讨论，下表给出了各种有返回结果的Stream结束操作。\n返回类型 对应的结束操作 boolean anyMatch() allMatch() noneMatch() Optional findFirst() findAny() 归约结果 reduce() collect() 数组 toArray() 对于表中返回boolean或者Optional的操作（Optional是存放一个值的容器）的操作，由于返回一个值，只需要在对应的Sink中记录这个值，等到执行结束时返回就可以了。 对于归约操作，最终结果放在用户调用时指定的容器中（容器类型通过[收集器](https://objcoding.com/2019/03/04/lambda/5-Streams API(II).md#收集器)指定）。collect(), reduce(), max(), min()都是归约操作，虽然max()和min()也是返回一个Optional，但事实上底层是通过调用[reduce()](https://objcoding.com/2019/03/04/lambda/5-Streams API(II).md#多面手reduce)方法实现的。 对于返回是数组的情况，毫无疑问的结果会放在数组当中。这么说当然是对的，但在最终返回数组之前，结果其实是存储在一种叫做Node的数据结构中的。Node是一种多叉树结构，元素存储在树的叶子当中，并且一个叶子节点可以存放多个元素。这样做是为了并行执行方便。关于Node的具体结构，我们会在下一节探究Stream如何并行执行时给出详细说明。 本文详细介绍了Stream流水线的组织方式和执行过程，学习本文将有助于理解原理并写出正确的Stream代码，同时打消你对Stream API效率方面的顾虑。如你所见，Stream API实现如此巧妙，即使我们使用外部迭代手动编写等价代码，也未必更加高效。\n9. parallelStream # Fork/Join 框架的核心是采用分治法的思想，将一个大任务拆分为若干互不依赖的子任务，把这些子任务分别放到不同的队列里，并为每个队列创建一个单独的线程来执行队列里的任务。同时，为了最大限度地提高并行处理能力，采用了工作窃取算法来运行任务，也就是说当某个线程处理完自己工作队列中的任务后，尝试当其他线程的工作队列中窃取一个任务来执行，直到所有任务处理完毕。所以为了减少线程之间的竞争，通常会使用双端队列，被窃取任务线程永远从双端队列的头部拿任务执行，而窃取任务的线程永远从双端队列的尾部拿任务执行。\n使用parallelStream的几个好处：\n代码优雅，可以使用lambda表达式，原本几句代码现在一句可以搞定； 运用多核特性(forkAndJoin)并行处理，大幅提高效率。 关于并行流和多线程的性能测试可以看一下下面的几篇博客： 并行流适用场景-CPU密集型 提交订单性能优化系列之006-普通的Thread多线程改为Java8的parallelStream并发流 然而，任何事物都不是完美的，并行流也不例外，其中最明显的就是使用(parallel)Stream极其不便于代码的跟踪调试，此外并行流带来的不确定性也使得我们对它的使用变得格外谨慎。我们得去了解更多的并行流的相关知识来保证自己能够正确的使用这把双刃剑。\nparallelStream使用时需要注意的点：\n适用CPU密集型的计算任务，不适用于IO密集型，特别的，对于CPU负载很大的情况也不适用。 不要在多线程中使用parallelStream，原因同上类似，大家都抢着CPU是没有提升效果，反而还会加大线程切换开销。 确保每条处理无状态且没有关联 使用并行流的时候是无法保证元素的顺序的 十三、特性 # Java 各版本的新特性 # New highlights in Java SE 8\nLambda Expressions Pipelines and Streams Date and Time API Default Methods Type Annotations Nashhorn JavaScript Engine Concurrent Accumulators Parallel operations PermGen Error Removed New highlights in Java SE 7\nStrings in Switch Statement Type Inference for Generic Instance Creation Multiple Exception Handling Support for Dynamic Languages Try with Resources Java nio Package Binary Literals, Underscore in literals Diamond Syntax Difference between Java 1.8 and Java 1.7? Java 8 特性 Java 与 C++ 的区别 # Java 是纯粹的面向对象语言，所有的对象都继承自 java.lang.Object，C++ 为了兼容 C 即支持面向对象也支持面向过程。 Java 通过虚拟机从而实现跨平台特性，但是 C++ 依赖于特定的平台。 Java 没有指针，它的引用可以理解为安全指针，而 C++ 具有和 C 一样的指针。 Java 支持自动垃圾回收，而 C++ 需要手动回收。 Java 不支持多重继承，只能通过实现多个接口来达到相同目的，而 C++ 支持多重继承。 Java 不支持操作符重载，虽然可以对两个 String 对象执行加法运算，但是这是语言内置支持的操作，不属于操作符重载，而 C++ 可以。 Java 的 goto 是保留字，但是不可用，C++ 可以使用 goto。 What are the main differences between Java and C++?\nJRE or JDK # JRE：Java Runtime Environment，java运行环境的简称，为java的运行提供了所需的环境。主要包括了JVM的标准实现和一些java基本类库。 JDK：Java Development Kit，java开发工具包，提供了java的开发及运行环境。JDK是java开发的核心，集成了JRE以及一些其他的工具，比如编译 java 源码的编译器 javac等。 因此可以这样认为：JDK\u0026gt;JRE\u0026gt;JVM，JRE支持了java程序的运行，而JDK则同时支持了java程序的开发。 参考资料 # Eckel B. Java 编程思想[M]. 机械工业出版社, 2002. Bloch J. Effective java[M]. Addison-Wesley Professional, 2017. ","date":"2021-06-25","externalUrl":null,"permalink":"/2021/java-basic/","section":"Posts","summary":"这篇文章总结了 Java 基础知识.","title":"Java 基础","type":"posts"},{"content":"","date":"2021-07-23","externalUrl":null,"permalink":"/categories/","section":"","summary":"","title":"","type":"categories"},{"content":"","date":"2021-07-23","externalUrl":null,"permalink":"/","section":"Charles' Blog","summary":"","title":"Charles' Blog","type":"page"},{"content":"","date":"2021-07-23","externalUrl":null,"permalink":"/categories/java/","section":"","summary":"","title":"Java","type":"categories"},{"content":"","date":"2021-07-23","externalUrl":null,"permalink":"/tags/java/","section":"Tags","summary":"","title":"Java","type":"tags"},{"content":"","date":"2021-07-23","externalUrl":null,"permalink":"/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":"","date":"2021-07-23","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"2021-07-23","externalUrl":null,"permalink":"/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/","section":"Tags","summary":"","title":"编程语言","type":"tags"},{"content":"","date":"2021-07-23","externalUrl":null,"permalink":"/categories/%E8%BD%AC%E8%BD%BD/","section":"","summary":"","title":"转载","type":"categories"},{"content":"","date":"2021-07-23","externalUrl":null,"permalink":"/tags/%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5/","section":"Tags","summary":"","title":"最佳实践","type":"tags"},{"content":" 作者很懒，什么都没有留下，敬请期待。。。 # ","date":"2021-06-24","externalUrl":null,"permalink":"/about/","section":"Charles' Blog","summary":"作者很懒，什么都没有留下，敬请期待。。。 # ","title":"About","type":"page"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"}]