那些年,老同志们踩过的 Java 坑

中国有句老话叫"事不过三",指一个人犯了同样的错误,一次两次三次还可以原谅,超过三次就不可原谅了。有人指出这个“三”是虚数,用来泛指多次,所以"事不过三"不包括“三”。至于"事不过三"包不包括“三”,可能跟每个人的底线有关系,属于哲学范畴,不在本文的讨论范围之内。

写代码也是如此,同一个代码“坑”,踩第一次叫"长了经验",踩第二次叫"加深印象",踩第三次叫"不长心眼",踩三次以上就叫"不可救药"。在本文中,笔者总结了一些代码坑,描述了问题现象,进行了问题分析,给出了避坑方法。希望大家在日常编码中,遇到了这类代码坑,能够提前避让开来。

1.对象比较方法

提供的方法,非常方便地实现了对象的比较,有效地避免了繁琐的空指针检查。

1.1.问题现象

在之前,在判断一个短整型、整型、长整型包装数据类型与常量是否相等时,我们一般这样写:

复制代码

ShortshortValue=(short)12345;(shortValue==12345);//(12345==shortValue);//trueIntegerintValue=12345;(intValue==12345);//(12345==intValue);//trueLonglongValue=12345L;(longValue==12345);//(12345==longValue);//true

从之后,提供了方法,并推荐使用函数式编程,更改代码如下:

复制代码

ShortshortValue=(short)12345;((shortValue,12345));//((12345,shortValue));//falseIntegerintValue=12345;((intValue,12345));//((12345,intValue));//trueLonglongValue=12345L;((longValue,12345));//((12345,longValue));//false

为什么直接把==替换为方法会导致输出结果不一样?

1.2.问题分析

通过反编译第一段代码,我们得到语句"(shortValue==12345);"的字节码指令如下:

复制代码

7:[22]10aload_1[shortValue]11():short[28]14sipush1234517if_icmpne2420iconst_121goto2524iconst_025(boolean):void[32]

原来,编译器会判断包装数据类型对应的基本数据类型,并采用这个基本数据类型的指令进行比较(比如上面字节码指令中的sipush和if_icmpne等),相当于编译器自动对常量进行了数据类型的强制转化。

为什么采用方法后,编译器不自动对常量进行数据类型的强制转化?通过反编译第二段代码,我们得到语句“((shortValue,12345));”的字节码指令如下:

复制代码

7:[22]10aload_1[shortValue]11(int):[28]17(,):boolean[33]20(boolean):void[39]

原来,编译器根据字面意思,认为常量12345默认基本数据类型是int,所以会自动转化为包装数据类型Integer。

下面来分析一下方法的代码实现:

复制代码

publicstaticbooleanequals(Objecta,Objectb){return(a==b)||(a!=(b));}

其中,语句“(b)”将会使用到方法。

方法的代码实现为:

复制代码

publicbooleanequals(Objectobj){if(objinstanceofShort){returnvalue==((Short)obj).shortValue();}returnfalse;}

通过代码实现分析:对应语句"((shortValue,12345));",因为的两个参数对象类型不一致,一个是包装数据类型Short,另一个是包装数据类型Integer,所以最终的比较结果必然是false。同样,语句“((intValue,12345));”,因为的两个参数对象类型一致,都是包装数据类型Integer且取值一样,所以最终的比较结果必然是true。

1.3.避坑方法

1、保持良好的编码习惯,避免数据类型的自动转化

为了避免数据类型自动转化,更科学的写法是直接声明常量为对应的基本数据类型。

第一段代码可以这样写:

复制代码

ShortshortValue=(short)12345;(shortValue==(short)12345);//((short)12345==shortValue);//trueIntegerintValue=12345;(intValue==12345);//(12345==intValue);//trueLonglongValue=12345L;(longValue==12345L);//(12345L==longValue);//true

第二段代码可以这样写:

复制代码

ShortshortValue=(short)12345;((shortValue,(short)12345));//(((short)12345,shortValue));//trueIntegerintValue=12345;((intValue,12345));//((12345,intValue));//trueLonglongValue=12345L;((longValue,12345L));//((12345L,longValue));//true

2、借助开发工具或插件,及早地发现数据类型不匹配问题

在Eclipse的问题窗口中,我们会看到这样的提示:

复制代码

Unlikelyargumenttypeforequals():intseemstobeunrelatedtoShortUnlikelyargumenttypeforequals():ShortseemstobeunrelatedtointUnlikelyargumenttypeforequals():intseemstobeunrelatedtoLongUnlikelyargumenttypeforequals():Longseemstobeunrelatedtoint

通过FindBugs插件扫描,我们会看到这样的警告:

复制代码

(Integer)(String[])[Scariest(1),Highconfidence](Short)(String[])[Scariest(1),Highconfidence](Integer)(String[])[Scariest(1),Highconfidence](Long)(String[])[Scariest(1),Highconfidence]

3、进行常规性单元测试,尽量把问题发现在研发阶段

“勿以善小而不为”,不要因为改动很小就不需要进行单元测试了,往往Bug都出现在自己过度自信的代码中。像这种问题,只要进行一次单元测试,是完全可以发现问题的。

2.三元表达式拆包

三元表达式是Java编码中的一个固定语法格式:“条件表达式?表达式1:表达式2”。三元表达式的逻辑为:“如果条件表达式成立,则执行表达式1,否则执行表达式2”。

2.1.问题现象

复制代码

booleancondition=false;Doublevalue1=1.0D;Doublevalue2=2.0D;Doublevalue3=null;Doubleresult=condition?value1*value2:value3;//抛出空指针异常

当条件表达式condition等于false时,直接把Double对象value3赋值给Double对象result,按道理没有问题呀,为什么会抛出空指针异常(NullPointerException)?

2.2.问题分析

通过反编译代码,我们得到语句"Doubleresult=condition?value1*value2:value3;"的字节码指令如下:

复制代码

17iload_1[condition]18ifeq3321aload_2[value1]22():double[24]25aload_3[value2]26():double[24]29dmul30goto3833aload4[value3]35():double[24]38(double):[16]41astore5[result]43:[28]46aload5[result]

在第33行,加载Double对象value3到操作数栈中;在第35行,调用Double对象value3的doubleValue方法。这个时候,由于value3是空对象null,调用doubleValue方法必然抛出抛出空指针异常。但是,为什么要把空对象value3转化为基础数据类型double?

查阅相关资料,得到三元表达式的类型转化规则:

根据规则分析,表达式1(value1*value2)计算后返回基础数据类型double,表达式2(value3)返回包装数据类型Double,根据三元表达式的类型转化规则判断,最终的返回类型为基础数据类型double。所以,当条件表达式condition等于false时,需要把空对象value3转化为基础数据类型double,于是就调用了value3的doubleValue方法抛出了空指针异常。

可以用以下案例验证三元表达式的类型转化规则:

复制代码

booleancondition=false;Doublevalue1=1.0D;Doublevalue2=2.0D;Doublevalue3=null;Integervalue4=null;//返回类型为Double,不抛出空指针异常Doubleresult1=condition?value1:value3;//返回类型为double,会抛出空指针异常Doubleresult2=condition?value1:value4;//返回类型为double,不抛出空指针异常Doubleresult3=!condition?value1*value2:value3;//返回类型为double,会抛出空指针异常Doubleresult4=condition?value1*value2:value3;
2.3.避坑方法

1、尽量避免使用三元表达式,可以采用if-else语句代替

如果三元表达式中有算术计算和包装数据类型,可以考虑利用if-else语句代替。改写代码如下:

复制代码

booleancondition=false;Doublevalue1=1.0D;Doublevalue2=2.0D;Doublevalue3=null;Doubleresult;if(condition){result=value1*value2;}else{result=value3;}

2、尽量使用基本数据类型,避免数据类型的自动转化

如果三元表达式中有算术计算和包装数据类型,可以考虑利用if-else语句代替。改写代码如下:

复制代码

booleancondition=false;doublevalue1=1.0D;doublevalue2=2.0D;doublevalue3=3.0D;doubleresult=condition?value1*value2:value3;

3、进行覆盖性单元测试,尽量把问题发现在研发阶段

像这种问题,只要编写一些单元测试用例,进行一些覆盖性测试,是完全可以提前发现的。

3.泛型对象赋值

Java泛型是中引入的一个新特性,其本质是参数化类型,即把数据类型做为一个参数使用。

3.1.问题现象

在做用户数据分页查询时,因为笔误编写了如下代码:

1、:

复制代码

/**分页数据VO类*/@Getter@Setter@ToString@NoArgsConstructor@AllArgsConstructorpublicclassPageDataVOT{/**总共数量*/privateLongtotalCount;/**数据列表*/privateListTdataList;}

2、:

复制代码

/**用户DAO接口*/@MapperpublicinterfaceUserDAO{/**统计用户数量*/publicLongcountUser(@Param("query")UserQueryVOquery);/**查询用户信息*/publicListUserDOqueryUser(@Param("query")UserQueryVOquery);}

3、:

复制代码

/**用户服务类*/@ServicepublicclassUserService{/**用户DAO*/@AutowiredprivateUserDAOuserDAO;/**查询用户信息*/publicPageDataVOUserVOqueryUser(UserQueryVOquery){ListUserDOdataList=null;LongtotalCount=(query);if((totalCount)(0L)0){dataList=(query);}returnnewPageDataVO(totalCount,dataList);}}

4、:

复制代码

/**用户控制器类*/@Controller@RequestMapping("/user")publicclassUserController{/**用户服务*/@AutowiredprivateUserServiceuserService;/**查询用户*/@ResponseBody@RequestMapping(value="/query",method=)publicResultPageDataVOUserVOqueryUser(@RequestBodyUserQueryVOquery){PageDataVOUserVOpageData=(query);(pageData);}}

以上代码没有任何编译问题,但是却把UserDO中一些涉密字段返回给前端。细心的读者可能已经发现了,在UserService类的queryUser方法的语句"returnnewPageDataVO(totalCount,dataList);"中,我们把ListUserDO对象dataList赋值给了PageDataVOUserVO的ListUserVO字段dataList。

问题是:为什么开发工具不报编译错误啦?

3.2.问题分析

由于历史原因,参数化类型和原始类型需要兼容。我们以ArrayList举例子,来看看如何兼容的。

以前的写法:

复制代码

ArrayListlist=newArrayList();

现在的写法:

复制代码

ArrayListStringlist=newArrayListString();

考虑到与以前的代码兼容,各种对象引用之间传值,必然会出现以下的情况:

复制代码

//第一种情况ArrayListlist1=newArrayListString();//第二种情况ArrayListStringlist2=newArrayList();

所以,Java编译器对以上两种类型进行了兼容,不会出现编译错误,但会出现编译告警。但是,我的开发工具在编译时真没出现过告警。

再来分析我们遇到的问题,实际上同时命中了两种情况:

1、把ListUserDO对象赋值给List,命中了第一种情况;

2、把PageDataVO对象赋值给PageDataVOUserVO,命中了第二种情况。

最终的效果就是:我们神奇地把ListUserDO对象赋值给了ListUserVO。

问题的根源就是:我们在初始化PageDataVO对象时,没有要求强制进行类型检查。

3.3.避坑方法

1、在初始化泛型对象时,推荐使用diamond语法

在《阿里巴巴Java开发手册》中,有这么一条推荐规则:

其实,初始化泛型对象时,全省略是不推荐的。这样会避免类型检查,从而造成上面的问题。

在初始化泛型对象时,推荐使用diamond语法,代码如下:

复制代码

returnnewPageDataVO(totalCount,dataList);

现在,在Eclipse的问题窗口中,我们会看到这样的错误:

复制代码

CannotinfertypeargumentsforPageDataVO

于是,我们就知道忘了把ListUserDO对象转化为ListUserVO对象了。

2、在进行单元测试时,需要对比数据内容

在进行单元测试时,运行正常是一个指标,但数据正确才是更重要的指标。

4.泛型属性拷贝

Spring的方法,是一个很好用的属性拷贝工具方法。

4.1.问题现象

根据数据库开发规范,数据库表格必须包含id,gmt_create,gmt_modified三个字段。其中,id这个字段,可能根据数据量不同,采用int或long类型(注意:阿里规范要求必须是long类型,这里为了举例说明,允许为int或long类型)。

所以,把这三个字段抽出来,定义了一个BaseDO基类:

复制代码

/**基础DO类*/@Getter@Setter@ToStringpublicclassBaseDOT{privateTid;privateDategmtCreate;privateDategmtModified;}

针对user表,定义了一个UserDO类:

复制代码

/**用户DO类*/@Getter@Setter@ToStringpublicclassUserDOextsBaseDOLong{privateStringname;privateStringdescription;}

对于查询接口,定义了一个UserVO类:

复制代码

/**用户VO类*/@Getter@Setter@ToStringpublicstaticclassUserVO{privateLongid;privateStringname;privateStringdescription;}

实现查询用户服务接口,实现代码如下:

复制代码

/**用户服务类*/@ServicepublicclassUserService{/**用户DAO*/@AutowiredprivateUserDAOuserDAO;/**查询用户*/publicListUserVOqueryUser(UserQueryVOquery){//查询用户信息ListUserDOuserDOList=(query);if(()){();}//转化用户列表ListUserVOuserVOList=newArrayList(());for(UserDOuserDO:userDOList){UserVOuserVO=newUserVO();(userDO,userVO);(userVO);}//返回用户列表returnuserVOList;}}

通过测试,我们会发现一个问题——调用查询用户服务接口,用户ID的值并没有返回。

复制代码

[{"description":"Thisisatester.","name":"tester"},]
4.2.问题分析

**

**按道理,UserDO类和UserVO类的id字段,类型都是Long类型,不存在类型不可转化,应该能够正常赋值。尝试手工赋值,代码如下:

复制代码

for(UserDOuserDO:userDOList){UserVOuserVO=newUserVO();(());(());(());(userVO);}

经过测试,上面代码返回结果正常,用户ID的值成功返回。

那么,就是工具方法的问题了。用Debug模式运行,进入到工具方法内部,得到以下数据:

原来,UserDO类的getId方法返回类型不是Long类型,而是被泛型还原成了Object类型。而下面的工具方法,判断是否能够把Object类型赋值给Long类型,当然会返回false导致不能进行属性拷贝。

为什么作者不考虑"先获取属性值,再判断能否赋值”?建议代码如下:

复制代码

Objectvalue=(source);if((value)(()[0],())){//赋值相关代码}
4.3.避坑方法

1、不要盲目地相信第三方工具包,任何工具包都有可能存在问题

在Java中,存在很多第三方工具包,比如:Apache的commons-lang3、commons-collections,Google的guava……都是很好用的第三方工具包。但是,不要盲目地相信第三方工具包,任何工具包都有可能存在问题。

2、如果需要拷贝的属性较少,可以手动编码进行属性拷贝

用反射拷贝属性,主要优点是节省了代码量,主要缺点是导致程序性能下降。所以,如果需要拷贝的属性较少,可以手动编码进行属性拷贝。

3、一定要进行单元测试,一定要对比数据内容

在编写完代码后,一定要进行单元测试,一定要对比数据内容。切莫想当然地认为:工具包很成熟、代码也很简单,不可能出现问题。

5.Set对象排重

在Java语言中,Set数据结构可以用于对象排重,常见的Set类有HashSet、LinkedHashSet等。

5.1.问题现象

编写了一个城市辅助类,从CSV文件中读取城市数据:

复制代码

/**城市辅助类*/@Slf4jpublicclassCityHelper{/**测试主方法*/publicstaticvoidmain(String[]args){CollectionCitycityCollection=readCities2("");((cityCollection));}/**读取城市*/publicstaticCollectionCityreadCities(StringfileName){try(FileInputStreamstream=newFileInputStream(fileName);InputStreamReaderreader=newInputStreamReader(stream,"GBK");CSVParserparser=newCSVParser(reader,())){SetCitycitySet=newHashSet(1024);IteratorCSVRecorditerator=();while(()){(parseCity(()));}returncitySet;}catch(IOExceptione){("读取所有城市异常",e);}();}/**解析城市*/privatestaticCityparseCity(CSVRecordrecord){Citycity=newCity();((0));((1));returncity;}/**城市类*/@Getter@Setter@ToStringprivatestaticclassCity{/**城市编码*/privateStringcode;/**城市名称*/privateStringname;}}

代码中使用HashSet数据结构,目的是为了避免城市数据重复,对读取的城市数据进行强制排重。

当输入文件内容如下时:

复制代码

编码,名称010,北京020,广州010,北京

解析后的JSON结果如下:

复制代码

[{"code":"010","name":"北京"},{"code":"020","name":"广州"},{"code":"010","name":"北京"}]

但是,并没有对城市“北京”进行排重。

5.2.问题分析

当向集合Set中增加对象时,首先集合计算要增加对象的hashCode,根据该值来得到一个位置用来存放当前对象。如在该位置没有一个对象存在的话,那么集合Set认为该对象在集合中不存在,直接增加进去。如果在该位置有一个对象存在的话,接着将准备增加到集合中的对象与该位置上的对象进行equals方法比较:如果该equals方法返回false,那么集合认为集合中不存在该对象,就把该对象放在这个对象之后;如果equals方法返回true,那么就认为集合中已经存在该对象了,就不会再将该对象增加到集合中了。所以,在哈希表中判断两个元素是否重复要使用到hashCode方法和equals方法。hashCode方法决定数据在表中的存储位置,而equals方法判断表中是否存在相同的数据。

分析上面的问题,由于没有重写City类的hashCode方法和equals方法,就会采用Object类的hashCode方法和equals方法。其实现如下:

复制代码

publicnativeinthashCode();publicbooleanequals(Objectobj){return(this==obj);}

可以看出:Object类的hashCode方法是一个本地方法,返回的是对象地址;Object类的equals方法只比较对象是否相等。所以,对于两条完全一样的北京数据,由于在解析时初始化了不同的City对象,导致hashCode方法和equals方法值都不一样,必然被Set认为是不同的对象,所以没有进行排重。

那么,我们就重写把City类的hashCode方法和equals方法,代码如下:

复制代码

/**城市类*/@Getter@Setter@ToStringprivatestaticclassCity{/**城市编码*/privateStringcode;/**城市名称*/privateStringname;/**判断相等*/@Overridepublicbooleanequals(Objectobj){if(obj==this){returntrue;}if((obj)){returnfalse;}if(()!=()){returnfalse;}(,((City)obj).code);}/**哈希编码*/@OverridepublicinthashCode(){();}}

重新支持测试程序,解析后的JSON结果如下:

复制代码

[{"code":"010","name":"北京"},{"code":"020","name":"广州"}]

结果正确,已经对城市“北京”进行排重。

5.3.避坑方法

1、当确定数据唯一时,可以使用List代替Set

当确定解析的城市数据唯一时,就没有必要进行排重操作,可以直接使用List来存储。

复制代码

ListCitycitySet=newArrayList(1024);IteratorCSVRecorditerator=();while(()){(parseCity(()));}returncitySet;

2、当确定数据不唯一时,可以使用Map代替Set

当确定解析的城市数据不唯一时,需要安装城市名称进行排重操作,可以直接使用Map进行存储。为什么不建议实现City类的hashCode方法,再采用HashSet来实现排重呢?首先,不希望把业务逻辑放在模型DO类中;其次,把排重字段放在代码中,便于代码的阅读、理解和维护。

复制代码

MapString,CitycityMap=newHashMap(1024);IteratorCSVRecorditerator=();while(()){Citycity=parseCity(());((),city);}();

3、遵循Java语言规范,重写hashCode方法和equals方法不重写hashCode方法和equals方法的自定义类不应该在Set中使用。

6.公有方法代理

SpringCGLIB代理生成的代理类是一个继承被代理类,通过重写被代理类中的非final的方法实现代理。所以,SpringCGLIB代理的类不能是final类,代理的方法也不能是final方法,这是由继承机制限制的。

6.1.问题现象

这里举例一个简单的例子,只有超级用户才有删除公司的权限,并且所有服务函数被AOP拦截处理异常。例子代码如下:

1、:

复制代码

/**用户服务类*/@ServicepublicclassUserService{/**超级用户*/privateUsersuperUser;/**设置超级用户*/publicvoidsetSuperUser(UsersuperUser){=superUser;}/**获取超级用户*/publicfinalUsergetSuperUser(){;}}

2、:

复制代码

/**公司服务类*/@ServicepublicclassCompanyService{/**公司DAO*/@AutowiredprivateCompanyDAOcompanyDAO;/**用户服务*/@AutowiredprivateUserServiceuserService;/**删除公司*/publicvoiddeleteCompany(LongcompanyId,LongoperatorId){//设置超级用户(newUser(0L,"admin","超级用户"));//验证超级用户if(!(operatorId,().getId())){thrownewExampleException("只有超级用户才能删除公司");}//删除公司信息(companyId,operatorId);}}

3、:

复制代码

/**AOP配置类*/@Slf4j@Aspect@ConfigurationpublicclassAopConfiguration{/**环绕方法*/@Around("execution(*.*.*(..))")publicObjectaround(ProceedingJoinPointjoinPoint){try{("开始调用服务方法");();}catch(Throwablee){((),e);thrownewExampleException((),e);}}}

当我们调用CompanyService的deleteCompany方法时,居然也抛出空指针异常(NullPointerException),因为调用UserService类的getSuperUser方法获取的超级用户为null。但是,我们在CompanyService类的deleteCompany方法中,每次都通过UserService类的setSuperUser方法强制指定了超级用户,按道理通过UserService类的getSuperUser方法获取到的超级用户不应该为null。其实,这个问题也是由AOP代理导致的。

6.2.问题分析

使用SpringCGLIB代理类时,Spring会创建一个名为UserService$EnhancerBySpringCGLIB$???的代理类。反编译这个代理类,得到以下主要代码:

复制代码

publicclassUserService$EnhancerBySpringCGLIB$a2c3b345extsUserServiceimplementsSpringProxy,Advised,Factory{publicfinalvoidsetSuperUser(Uservar1){MethodInterceptorvar10000=$CALLBACK_0;if(var10000==null){CGLIB$BIND_CALLBACKS(this);var10000=$CALLBACK_0;}if(var10000!=null){(this,CGLIB$setSuperUser$0$Method,newObject[]{var1},CGLIB$setSuperUser$0$Proxy);}else{(var1);}}}

可以看出,这个代理类继承了UserService类,代理了setSuperUser方法,但是没有代理getSuperUser方法。所以,当我们调用setSuperUser方法时,设置的是原始对象实例的superUser字段值;而当我们调用getSuperUser方法时,获取的是代理对象实例的superUser字段值。如果把这两个方法的final修饰符互换,同样存在获取超级用户为null的问题。

6.3.避坑方法

1、严格遵循CGLIB代理规范,被代理的类和方法不要加final修饰符

严格遵循CGLIB代理规范,被代理的类和方法不要加final修饰符,避免动态代理操作对象实例不同(原始对象实例和代理对象实例),从而导致数据不一致或空指针问题。

2、缩小CGLIB代理类的范围,能不用被代理的类就不要被代理

缩小CGLIB代理类的范围,能不用被代理的类就不要被代理,即可以节省内存开销,又可以提高函数调用效率。

7.公有字段代理

在fastjson强制升级到1.2.60时踩过一个坑,作者为了开发快速,在ParseConfig中定义了:

复制代码

publicclassParseConfig{publicfinalSymbolTablesymbolTable=newSymbolTable(4096);}

在我们的项目中继承了该类,同时又被AOP动态代理了,于是一行代码引起了一场“血案”。

7.1.问题现象

仍然使用上章的例子,但是把获取、设置方法删除,定义了一个公有字段。例子代码如下:

1、:

复制代码

/**用户服务类*/@ServicepublicclassUserService{/**超级用户*/publicfinalUsersuperUser=newUser(0L,"admin","超级用户");}

2、:

复制代码

/**公司服务类*/@ServicepublicclassCompanyService{/**公司DAO*/@AutowiredprivateCompanyDAOcompanyDAO;/**用户服务*/@AutowiredprivateUserServiceuserService;/**删除公司*/publicvoiddeleteCompany(LongcompanyId,LongoperatorId){//验证超级用户if(!(operatorId,())){thrownewExampleException("只有超级用户才能删除公司");}//删除公司信息(companyId,operatorId);}}

3、:

同上一章。

当我们调用CompanyService的deleteCompany方法时,居然抛出空指针异常(NullPointerException)。经过调试打印,发现是UserService的superUser变量为null。如果把AopConfiguration删除,就不会出现空指针异常,说明这个问题是由AOP代理导致的。

7.2.问题分析

使用SpringCGLIB代理类时,Spring会创建一个名为UserService$EnhancerBySpringCGLIB$???的代理类。这个代理类继承了UserService类,并覆盖了UserService类中的所有非final的public的方法。但是,这个代理类并不调用super基类的方法;相反,它会创建的一个成员userService并指向原始的UserService类对象实例。现在,内存中存在两个对象实例:一个是原始的UserService对象实例,另一个指向UserService的代理对象实例。这个代理类只是一个虚拟代理,它继承了UserService类,并且具有与UserService相同的字段,但是它从来不会去初始化和使用它们。所以,一但通过这个代理类对象实例获取公有成员变量时,将返回一个默认值null。

7.3.避坑方法

1、当确定字段不可变时,可以定义为公有静态常量

当确定字段不可变时,可以定义为公有静态常量,并用类名称+字段名称访问。类名称+字段名称访问公有静态常量,与类实例的动态代理无关。

复制代码

/**用户服务类*/@ServicepublicclassUserService{/**超级用户*/publicstaticfinalUserSUPER_USER=newUser(0L,"admin","超级用户");}/**使用代码*/if(!(operatorId,_())){thrownewExampleException("只有超级用户才能删除公司");}

2、当确定字段不可变时,可以定义为私有成员变量

当确定字段不可变时,可以定义为私有成员变量,提供一个公有方法获取该变量值。当该类实例被动态代理时,代理方法会调用被代理方法,从而返回被代理类的成员变量值。

复制代码

/**用户服务类*/@ServicepublicclassUserService{/**超级用户*/privateUsersuperUser=newUser(0L,"admin","超级用户");/**获取超级用户*/publicUsergetSuperUser(){;}}/**使用代码*/if(!(operatorId,().getId())){thrownewExampleException("只有超级用户才能删除公司");}

3、遵循JavaBean编码规范,不要定义公有成员变量

遵循JavaBean编码规范,不要定义公有成员变量。JavaBean规范如下:

后记

人类受益于“类比”思维,举一反三就是人类的智慧,每当遇到新生事物时,人们往往用类似的已知事物作为参考,能够加速对新生事物的认知。而人类又受制于“定势”思维,因为已知事物并不能代表新生事物,而人们又容易形成先入为主的概念,最终导致对新生事物产生误判。

陈昌毅,花名常意,高德地图技术专家,2018年加入阿里巴巴,一直从事地图数据采集的相关工作。

本文转载自公众号阿里巴巴中间件(ID:Aliware_2018)。

;mid=2247489196idx=1sn=1055ad532d6da3408b97aecd50fd637dchksm=fdeb24ccca9caddaadb4c7ed68a54b6789badc2b349f40e775f422cc2403a13249d56ea53e27scene=27#wechat_redirect

发布于 2025-05-14
3
下一篇:网络变压器介绍
目录

    推荐阅读