NYC's Blog - Coding http://niyanchun.com/category/coding/ zh-CN 编程~ Sun, 20 Feb 2022 22:55:00 +0800 Sun, 20 Feb 2022 22:55:00 +0800 如何修改Pandas中列的类型 http://niyanchun.com/how-to-change-column-data-type-in-pandas.html http://niyanchun.com/how-to-change-column-data-type-in-pandas.html Sun, 20 Feb 2022 22:55:00 +0800 NYC 问题如题。一般pandas读取数据的时候,会自动检测数据的类型,但有时候可能不是特别准确,还需要我们自己做类型转换。比如下面这种:

a = [['a', '1.2', '4.2'], ['b', '70', '0.03'], ['x', '5', '0']]
df = pd.DataFrame(a)

df.dtypes
# 0    object
# 1    object
# 2    object
# dtype: object

如何修改第2、3列的类型?扩展一下,如果有很多列的时候,如何高效的修改?

Pandas中主要有4种类型转换相关的方法:

  1. to_numeric/to_datetime/to_timedelta:可以参数转换为合适的对应类型。
  2. astype
  3. infer_objects
  4. convert_dtypes

to_xxx

to_numeric

to_numeric将参数转换为合适的数值类型(float64/int64)。签名如下:

pandas.to_numeric(arg, errors='raise', downcast=None)

先看一些使用例子:

In [2]: s = pd.Series(["8", 6, "7.5", 3, "0.9"])

In [3]: s
Out[3]:
0      8
1      6
2    7.5
3      3
4    0.9
dtype: object

In [4]: pd.to_numeric(s)
Out[4]:
0    8.0
1    6.0
2    7.5
3    3.0
4    0.9
dtype: float64

可以使用apply()方法批量转换DataFrame里面的列:

In [11]: df = pd.DataFrame([['1', '2', '3'],['4', '5', '6'],['7.1', '8.0', '9']], columns=['a','b', 'c'])

In [12]: df
Out[12]:
     a    b  c
0    1    2  3
1    4    5  6
2  7.1  8.0  9

In [13]: df.dtypes
Out[13]:
a    object
b    object
c    object
dtype: object

In [14]: df_1=df.apply(pd.to_numeric)

In [15]: df_1
Out[15]:
     a    b  c
0  1.0  2.0  3
1  4.0  5.0  6
2  7.1  8.0  9

In [16]: df_1.dtypes
Out[16]:
a    float64
b    float64
c      int64
dtype: object

也可以只对某些列进行转换:

In [18]: df[['a','b']]=df[['a','b']].apply(pd.to_numeric)

In [19]: df
Out[19]:
     a    b  c
0  1.0  2.0  3
1  4.0  5.0  6
2  7.1  8.0  9

In [20]: df.dtypes
Out[20]:
a    float64
b    float64
c     object
dtype: object

类型转换难免会产生错误,比如无法转换等,to_numeric提供了一个参数errors来让用户控制发生错误时如何处理,用有三个选项:

  • 'raise':默认值,即抛出异常
  • 'ignore':转换失败时,保留原值
  • 'coerce':转换失败时,设置为NaN

看一些例子:

In [21]: df=pd.DataFrame([['1','2'],['3','4'],['5','s']], columns=['a','b'])

In [22]: df
Out[22]:
   a  b
0  1  2
1  3  4
2  5  s

In [23]: df.dtypes
Out[23]:
a    object
b    object
dtype: object

In [24]: df.apply(pd.to_numeric)
ValueError: Unable to parse string "s" at position 2

In [25]: df.apply(pd.to_numeric, errors='coerce')
Out[25]:
   a    b
0  1  2.0
1  3  4.0
2  5  NaN

In [26]: df.apply(pd.to_numeric, errors='ignore')
Out[26]:
   a  b
0  1  2
1  3  4
2  5  s

to_numeric默认会转换为float64或者int64,如果你想节省内存转换为小一些的类型的话,可以使用to_numeric提供的downcast参数,可选值如下:

  • 'integer'或者'signed':转换为np.int8
  • 'unsigned':转换为np.uint8
  • 'float':转换为np.float32

看一些例子:

In [29]: s = pd.Series(['1','2','-7'])

In [30]: s
Out[30]:
0     1
1     2
2    -7
dtype: object

In [31]: pd.to_numeric(s)
Out[31]:
0    1
1    2
2   -7
dtype: int64

In [32]: pd.to_numeric(s, downcast='integer')
Out[32]:
0    1
1    2
2   -7
dtype: int8

# 注意这里:因为 unsigned无法表示-7,所以这里实际没有发生downcast
In [33]: pd.to_numeric(s, downcast='unsigned')
Out[33]:
0    1
1    2
2   -7
dtype: int64

In [34]: pd.to_numeric(s, downcast='float')
Out[34]:
0    1.0
1    2.0
2   -7.0
dtype: float32

这里有2个注意点:

  1. downcast是发生在核心的类型转换之后的(也就是先将原始类型转换为float64/int64,然后再执行downcast动作),所以前面介绍的那个errors参数对downcast这里是无效的。
  2. 如果目标类型无法容纳被转换的值,就不会发生实际的转换。比如上面尝试转换为'unsigned'类型时,因为-7无法转换为unsigned,所以实际没有执行downcast。

    to_datetime

    to_datetime把参数转换为datetime类型,相比于to_numeric,函数原型复杂了一些。

    pandas.to_datetime(arg, errors='raise', dayfirst=False, yearfirst=False, utc=None, format=None, exact=True, unit=None, infer_datetime_format=False, origin='unix', cache=True)

    看一些使用例子:

    # 可以使用这些关键字来构造表示时间日期的字典:[‘year’, ‘month’, ‘day’, ‘minute’, ‘second’, ‘ms’, ‘us’, ‘ns’]),复数也可以
    In [35]: df = pd.DataFrame({'year': [2015, 2016], 'month': [2, 3], 'day':[4, 5]})
    
    In [36]: df
    Out[36]:
  3. 2015 2 4
  4. 2016 3 5

    In [37]: df.dtypes
    Out[37]:
    year int64
    month int64
    day int64
    dtype: object

    In [38]: pd.to_datetime(df)
    Out[38]:

  5. 2015-02-04
  6. 2016-03-05
    dtype: datetime64[ns]

    In [40]: pd.to_datetime(1490195805, unit='s')
    Out[40]: Timestamp('2017-03-22 15:16:45')

    In [41]: pd.to_datetime(1490195805433502912, unit='ns')
    Out[41]: Timestamp('2017-03-22 15:16:45.433502912')

    In [42]: pd.to_datetime("10/11/12", dayfirst=True)
    Out[42]: Timestamp('2012-11-10 00:00:00')

    In [43]: pd.to_datetime("10/11/12", yearfirst=True)
    Out[43]: Timestamp('2010-11-12 00:00:00')

    In [44]: pd.to_datetime("10/11/12", dayfirst=True, yearfirst=True)
    Out[44]: Timestamp('2010-12-11 00:00:00')

    `errors`字段含义同`to_numeric`:
  • 'raise':默认值,即抛出异常
  • 'ignore':转换失败时,保留原值
  • 'coerce':转换失败时,设置为NaT。这里注意一种情况就是Pandas的时间精度是纳秒,且用int64表示,大概只能表示584年这样一个范围([pd.Timestamp.min, pd.Timestamp.max]),当转换的时间超出这个范围时也算失败。

看个例子:

In [46]: pd.Timestamp.min
Out[46]: Timestamp('1677-09-21 00:12:43.145225')

In [47]: pd.Timestamp.max
Out[47]: Timestamp('2262-04-11 23:47:16.854775807')

In [48]: pd.to_datetime('13000101', format='%Y%m%d', errors='raise')
OutOfBoundsDatetime: Out of bounds nanosecond timestamp: 1300-01-01 00:00:00

In [49]: pd.to_datetime('13000101', format='%Y%m%d', errors='ignore')
Out[49]: datetime.datetime(1300, 1, 1, 0, 0)

In [50]: pd.to_datetime('13000101', format='%Y%m%d', errors='coerce')
Out[50]: NaT

In [53]: pd.to_datetime('130000101', format='%Y%m%d', errors='ignore')
Out[53]: '130000101'

to_timedelta

timedelta类型表示两个时间的绝对差值,to_timedelta将参数转换为timedelta类型。方法签名如下:

pandas.to_timedelta(arg, unit=None, errors='raise')

unit用于指定参数的单位,默认为ns,合法的取值如下:

  • ‘W’
  • ‘D’ / ‘days’ / ‘day’
  • ‘hours’ / ‘hour’ / ‘hr’ / ‘h’
  • ‘m’ / ‘minute’ / ‘min’ / ‘minutes’ / ‘T’
  • ‘S’ / ‘seconds’ / ‘sec’ / ‘second’
  • ‘ms’ / ‘milliseconds’ / ‘millisecond’ / ‘milli’ / ‘millis’ / ‘L’
  • ‘us’ / ‘microseconds’ / ‘microsecond’ / ‘micro’ / ‘micros’ / ‘U’
  • ‘ns’ / ‘nanoseconds’ / ‘nano’ / ‘nanos’ / ‘nanosecond’ / ‘N’

errors的取值同to_datetime。看几个使用例子:

In [55]: pd.to_timedelta('1 days 06:05:01.00003')
Out[55]: Timedelta('1 days 06:05:01.000030')

In [56]: pd.to_timedelta('15.5us')
Out[56]: Timedelta('0 days 00:00:00.000015500')

In [57]: pd.to_timedelta(['1 days 06:05:01.00003', '15.5us', 'nan'])
Out[57]: TimedeltaIndex(['1 days 06:05:01.000030', '0 days 00:00:00.000015500', NaT], dtype='timedelta64[ns]', freq=None)

In [58]: import numpy as np

In [59]: pd.to_timedelta(np.arange(5), unit='s')
Out[59]:
TimedeltaIndex(['0 days 00:00:00', '0 days 00:00:01', '0 days 00:00:02',
                '0 days 00:00:03', '0 days 00:00:04'],
               dtype='timedelta64[ns]', freq=None)

In [60]: pd.to_timedelta(np.arange(5), unit='d')
Out[60]: TimedeltaIndex(['0 days', '1 days', '2 days', '3 days', '4 days'], dtype='timedelta64[ns]', freq=None)

astype

astype方法可以做任意类型的转换(当然未必能成功)。方法原型如下:

pd.DataFrame.astype(dtype, copy=True, errors='raise')

dtype就是我们想要转换成的目标类型,可以使用Numpy的类型、Python的部分类型、Pandas特有的类型。copy表示是否修改原数据。errors可以取'raise'(失败是抛异常)或者'ignore'(失败时忽略并返回原值)。

看一些例子:

In [62]: df = pd.DataFrame({'col1': [1, 2], 'col2': [3, 4]})

In [63]: df.dtypes
Out[63]:
col1    int64
col2    int64
dtype: object

In [64]: df.astype('int32').dtypes
Out[64]:
col1    int32
col2    int32
dtype: object

In [65]: s = pd.Series([1, 3], dtype='int32')

In [66]: s
Out[66]:
0    1
1    3
dtype: int32

In [67]: s.astype('int64')
Out[67]:
0    1
1    3
dtype: int64

In [68]: s.astype('category')
Out[68]:
0    1
1    3
dtype: category
Categories (2, int64): [1, 3]

In [77]: s_date = pd.Series(['20220101', '20220102', '20220103'])

In [78]: s_date
Out[78]:
0    20220101
1    20220102
2    20220103
dtype: object

In [79]: s_date.astype('datetime64')
Out[79]:
0   2022-01-01
1   2022-01-02
2   2022-01-03
dtype: datetime64[ns]

In [81]: i = pd.Series([1,2,3])

In [82]: i
Out[82]:
0    1
1    2
2    3
dtype: int64

In [83]: i.astype(str)
Out[83]:
0    1
1    2
2    3
dtype: object

In [84]: s = pd.Series([1,2,-7])

In [85]: s.astype(np.int8)
Out[85]:
0    1
1    2
2   -7
dtype: int8

In [86]: s.astype(np.uint8)
Out[86]:
0      1
1      2
2    249
dtype: uint8

使用astype的时候,要注意范围,比如下面的转换不会报错,但不是我们想要的:

In [95]: s = pd.Series([1,2,-7])

# -7被转换为249
In [96]: s.astype(np.uint8)
Out[96]:
0      1
1      2
2    249
dtype: uint8

# 使用to_numeric就不会有问题
In [97]: pd.to_numeric(s, downcast='unsigned')
Out[97]:
0    1
1    2
2   -7
dtype: int64

infer_objects

pandas 0.21.0版本加入,会尝试推测类型。看个例子:

In [103]: df = pd.DataFrame({'a': [7, 1, 5], 'b': ['3','2','1']}, dtype='object')

In [104]: df.dtypes
Out[104]:
a    object
b    object
dtype: object

In [105]: df.infer_objects().dtypes
Out[105]:
a     int64
b    object
dtype: object

可以看到,infer_objects其实还是比较“肤浅”的,如果要将'b'列也转换成数值型,可以使用前面介绍的方法。

convert_dtypes

convert_dtypes会尝试将各列转换为最可能的类型,其特点是支持pd.NA,方法签名如下:

DataFrame.convert_dtypes(infer_objects=True, convert_string=True, convert_integer=True, convert_boolean=True, convert_floating=True)

通过各个类型参数可以控制某些类型是否转换;infer_objects为True时会尝试转换object类型为更具体的类型。看一些例子:

In [110]: df = pd.DataFrame(
     ...:   {
     ...:     'a': pd.Series([1, 2, 3], dtype=np.dtype('int32')),
     ...:     'b': pd.Series(['x', 'y', 'z'], dtype=np.dtype('O')),
     ...:     'c': pd.Series([True, False, np.nan], dtype=np.dtype('O')),
     ...:     'd': pd.Series(['h', 'i', np.nan], dtype=np.dtype('O')),
     ...:     'e': pd.Series([10, np.nan, 20], dtype=np.dtype('float')),
     ...:     'f': pd.Series([np.nan, 100.5, 200], dtype=np.dtype('float')),
     ...:   }
     ...: )

In [111]: df
Out[111]:
   a  b      c    d     e      f
0  1  x   True    h  10.0    NaN
1  2  y  False    i   NaN  100.5
2  3  z    NaN  NaN  20.0  200.0

In [112]: df.dtypes
Out[112]:
a      int32
b     object
c     object
d     object
e    float64
f    float64
dtype: object

In [113]: dfn = df.convert_dtypes()

In [114]: dfn
Out[114]:
   a  b      c     d     e      f
0  1  x   True     h    10    NaN
1  2  y  False     i  <NA>  100.5
2  3  z   <NA>  <NA>    20  200.0

In [115]: dfn.dtypes
Out[115]:
a      Int32
b     string
c    boolean
d     string
e      Int64
f    float64
dtype: object

End, that's all!

refs: StackOverflow: Change column type in pandas

]]>
0 http://niyanchun.com/how-to-change-column-data-type-in-pandas.html#comments http://niyanchun.com/feed/category/coding/
如何disable Spring Security http://niyanchun.com/how-to-disable-spring-security.html http://niyanchun.com/how-to-disable-spring-security.html Sat, 06 Nov 2021 21:51:00 +0800 NYC 如题:如何disable Spring Security?(不要问为什么有这种场景,问了也不知道...)方法很多,比如下面这种在Spring Boot 2中已经过期的(不要再用了):

security.basic.enabled: false
management.security.enabled: false

Spring Boot 2中比较常见的是类似下面这种方式:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  @Value("${security.enabled:true}")
  private boolean securityEnabled;

  @Override
  public void configure(WebSecurity web) throws Exception {
    if (!securityEnabled) {
      // security.enabled=false的时候,所有url都直接放行(注意WebSecurity的优先级高于HttpSecurity)
      web.ignoring().antMatchers("/**");
    }
  }

  // ...省略其它代码 
}

本文到此...可能...还没有结束。如果你的代码里面使用了@PreAuthorize@PostAuthorize,并且启用了(@EnableGlobalMethodSecurity(prePostEnabled = true)),那按照上述方式修改以后,会出现下面的异常:

org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.credentialsNotFound(AbstractSecurityInterceptor.java:333)
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:200)
    at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:58)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692)
    ... ...

也就是如果使用了@PreAuthorize@PostAuthorize,还需要忽略这些注解。怎么办呢?如下:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  @Value("${security.enabled:true}")
  private boolean securityEnabled;


  @Override
  public void configure(WebSecurity web) throws Exception {
    if (!securityEnabled) {
      web.ignoring().antMatchers("/**");
    }
  }

  // ...省略其它代码 

  /**
   * control @EnableGlobalMethodSecurity(prePostEnabled = true),to  solve AuthenticationCredentialsNotFoundException
   */
  @ConditionalOnProperty(prefix = "security", name = "enabled", havingValue = "true")
  @EnableGlobalMethodSecurity(prePostEnabled = true)
  static class Dummy {
      // 什么也不做
  }
}

即搞一个空类/Bean来控制@EnableGlobalMethodSecurity(prePostEnabled = true),当security.enabled=false的时候,Dummy都没了,依托于它的注解自然也就没了。同理也可以控制@Secured等注解。至于本文提到的那些注解的功能就不赘述了,能看此文的自然都懂。

本文到此真的完了。

]]>
0 http://niyanchun.com/how-to-disable-spring-security.html#comments http://niyanchun.com/feed/category/coding/
Java内部类 http://niyanchun.com/java-nested-class.html http://niyanchun.com/java-nested-class.html Sat, 28 Nov 2020 20:02:00 +0800 NYC 基础

Java支持类中嵌套类,称之为nested class。嵌套的层数没有限制,但实际中一般最多用两层。根据内部类是否有static修饰,分为static nested classnon-static nested class。non-static nested class又被称为inner class。inner class里面又有两个特殊一点的类:local classanonymous class。特殊之处主要在于语法使用上,实质功能是差不多的。官方是这样解释的:

Nested classes are divided into two categories: static and non-static. Nested classes that are declared static are called static nested classes. Non-static nested classes are called inner classes.

用图表示分类及关系如下:

java-nested-class

上面是按照官方的说法来分的,实际中很多人习惯把所有的嵌套类都称为inner class(内部类),这只是个称谓,不影响沟通就好。后文用nested class来统称static nested class和non-static nested class。各个类的主要特性和限制已经在图中说明了(适用于JDK 8及以后)。

那为什么需要内部类呢?内部类是在Java 1.1中引入的,当时很多人质疑该设计增加了Java的复杂性,但实用性不强,当然这种问题仁者见仁智者见智。官方的解释是这样的:

Why Use Nested Classes?

Compelling reasons for using nested classes include the following:

  • It is a way of logically grouping classes that are only used in one place: If a class is useful to only one other class, then it is logical to embed it in that class and keep the two together. Nesting such "helper classes" makes their package more streamlined.**
  • It increases encapsulation: Consider two top-level classes, A and B, where B needs access to members of A that would otherwise be declared private. By hiding class B within class A, A's members can be declared private and B can access them. In addition, B itself can be hidden from the outside world.
  • It can lead to more readable and maintainable code: Nesting small classes within top-level classes places the code closer to where it is used.

总的来说,嵌套类最大的目的是改善代码的组织,并不是必不可少的功能。嵌套类能实现的功能,通过正常的类都可以实现,只不过可能要多写点代码,且不是很优雅而已。

为什么inner class可以访问外层类的非静态成员

要说明的是:嵌套类是对于编译器而言的,对虚拟机来说没有什么嵌套类,只有正常的类。也就是说嵌套类经过编译器编译之后要转化成一个个正常的类,比如A类里面嵌套了B类。那经过编译器之后会变成两个独立的类:AA$B。这样问题就很简单了,B类要访问A类的非静态成员,要满足两个条件:

  1. 要有A类的实例。
  2. 且要有访问成员的权限或者方式。

而编译器在编译期间就帮我们干了这两件事,下面验证一下。

定义一个嵌套类:

public class OuterClass {
    /**
    * 定义一个公有成员变量
    */
    public Object publicVariable = "public member variable";
    private Object privateVariable = "private member variable";
    /**
    * 定义两个私有成员变量
    */ 
    private Object privateVariable2 = "private member variable2";

    /**
    * 定义一个私有成员方法
    * @param parameter
    */
    private void privateMethod(String parameter) {
        System.out.println(parameter);
    }

    /**
    * 调用inner class
    */
    public void show() {
        InnerClass innerClass =  new InnerClass();
        innerClass.print();
    }

    /**
    * inner class
    */
    class InnerClass {
        void print() {
            // inner class里面直接调用了外部类的私有成员变量和成员方法
            System.out.println(privateVariable);
            privateMethod("invoke outer class private method.");
            // 调用外层类的公有变量
            System.out.println(publicVariable);
        }
    }

    public static void main(String[] args) {
        new OuterClass().show();
    }
}

上面代码定义了外层类是OuterClass,内部类是InnerClass。外层类定义了1个公有变量、2个私有变量和1个私有方法。然后在InnerClass里面直接使用了OuterClass的所有成员。程序运行结果如下:

private member variable
invoke outer class private method.
public member variable

反编译一下上面的两个类:

# javap -p OuterClass.class       
Compiled from "OuterClass.java"
public class OuterClass {
    public java.lang.Object publicVariable;
    private java.lang.Object privateVariable;
    private java.lang.Object privateVariable2;
    public OuterClass();
    private void privateMethod(java.lang.String);
    public void show();
    public static void main(java.lang.String[]);
    # 注意这两个静态方法
    static java.lang.Object access$000(OuterClass);
    static void access$100(OuterClass, java.lang.String);
}


# javap -p OuterClass\$InnerClass.class
Compiled from "OuterClass.java"
class OuterClass$InnerClass {
    # 注意这个final的成员变量和下面的构造函数
    final OuterClass this$0;
    OuterClass$InnerClass(OuterClass);
    void print();
}

结论就是:

  1. 编译器修改了内部类:

    • 增加了一个final的外层类实例作为内部类的成员变量;
    • 修改了内部类的构造函数,将外部类实例通过内部类的构造函数传递给内部类。这样内部类就有了外部类的实例,上面的第1个条件就达成了。
  2. 编译器在外部类中增加了几个非private的静态方法。对于内部类访问外部类的每一个私有成员,都会有这么一个方法。这样内部类就可以通过这些静态方法去访问外部类的私有成员了。非私有的成员直接通过1中的外层类实例即可访问,所以就无需生成这些静态方法了。

再进一步验证一下上面的结论1,当执行InnerClass innerClass = new InnerClass();语句创建一个内部类实例之后,可以观测到下面的结果:

inner-class-instance

可以看到,内部类实例(OuterClass$InnerClass@470)自动引用了外层类实例(OuterClass@464)。所以,inner class之所以能访问外层类的成员是因为它在实例化的时候就已经和一个外层类的实例关联了,实际是通过这个外层类实例去访问外层类的成员。对于私有成员,还生成了一些辅助的静态方法。这也说明,要实例化inner class,必须先实例化它的外层类。

另外有个限制就是inner class里面不能定义静态变量和静态方法,一个例外是可以定义基础类型和String类型的静态常量。比如:

static final String s = “s”;        // OK
static final int i = 5;    // OK
static final Integer ii = 5;    // 错误

local class和anonymous class都属于特殊的inner class,所以上面讲述的所有东西对他们也适用。

为什么static nested class不能访问外层类的非静态成员

原因很简单,static nested class除了被定义到某个类里面以外,几乎和普通的类没有什么区别。它不会和外层类的某个实例关联,比如我们在上面的OuterClass里面再定义一个StaticNestedClass

static class StaticNestedClass {
    private int a;
    void foo() {}
}

反编译以后:

# javap -p OuterClass\$StaticNestedClass.class
Compiled from "OuterClass.java"
class OuterClass$StaticNestedClass {
    private int a;
    OuterClass$StaticNestedClass();
    void foo();
}

除了类名被改写了以外,和原来定义的类没有任何区别。所以如果没有被定义为private的话,static nested class完全可以独立于外层类使用。

所有nested class都可以访问外层类的静态成员

上面讨论的都是nested class能不能访问外层类的非静态成员,那如果是静态成员呢?结论就是所有nested class都可以访问的静态成员,不管是不是私有。原理的话和inner class访问外层类非static成员是一样的,如果是private的,编译器会在外层中生成一个辅助访问的static方法,如果是非私有的,那通过类就可以直接访问。

## 如果nested class是private的?

我们知道正常的类是不能使用private和protected的(只能是public或者不加访问修饰符),但nested class却可以,因为nested class其实就是外层类的一个特殊成员,就像成员变量、成员方法一样。比如,如果我们不想让外部的其它类看到nested class的类,就可以将它设置成private的,但对于外层类是没有影响的,照样可以操作这个类。这个怎么做到的呢?

我们将上面的StaticNestedClass改为private的:

private static class StaticNestedClass {
   void foo() {
       System.out.println(a);
   }
}

反编译看下:


# javap -p OuterClass\$StaticNestedClass.class
Compiled from "OuterClass.java"
class OuterClass$StaticNestedClass {
    private OuterClass$StaticNestedClass();
    void foo();
    OuterClass$StaticNestedClass(OuterClass$1);
}

可以看到,如果nested class被设置成private,它原来的构造函数就会被设置为private的,同时编译器又新增了一个外部可见的构造函数OuterClass$StaticNestedClass(OuterClass$1),这个构造函数的一个入参就是外部类的实例。这样,外部类实例化nested class的时候会先调用这个构造函数,这个构造函数内部又调用了原始的private的构造函数。inner class也是这样的。

总结

嵌套类的实质就是外层类的成员,就像成员变量、成员方法一样,初衷是提高代码结构的紧凑和可维护性。使用嵌套类的几乎唯一的场景就是这个内部类仅供外层类(或者包含它的代码块)使用,其它场景都应该使用正常的一级类。按这个思路使用即可,不要滥用,更不要搞骚操作。

Reference

]]>
2 http://niyanchun.com/java-nested-class.html#comments http://niyanchun.com/feed/category/coding/
Java的单例模式 http://niyanchun.com/singleton-in-java.html http://niyanchun.com/singleton-in-java.html Sun, 21 Jul 2019 14:39:00 +0800 NYC Java的单例模式(Singleton Pattern)是指在任何情况下,都只有一个类实例存在。该模式也是众多设计模式中最简单的模式之一,但其中还是有不少门道,今天做一个学习总结。

单例模式的多种实现方式

单例模式的实现方式众多,一般的套路就是在常规的类上面增加三个特性:

  • 将构造方法设为private
  • 包含一个静态的类实例字段作为唯一的一个类实例,
  • 如果上一条中的类实例是private的,那就还需要一个静态工厂方法,用于返回该实例字段;如果是public的,那就不需要这一条了。

另外,不同的实现方法也有不同的效果,主要从是否多线程安全是否惰性初始化(直到第一次访问时才初始化它的值)、性能高低三个方面考量。下面来看各个实现方式。

方式1

public class Singleton1 {
    /**
     * 一个静态的类实例字段
     */
    private static Singleton1 INSTANCE;

    /**
     * 私有的构造方法
     */
    private Singleton1() {
    }

    /**
     * public的工厂方法
     *
     * @return Singleton1
     */
    public Singleton1 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton1();
        }

        return INSTANCE;
    }

    // 省略类的其他一些字段和方法
}

首先说明:在多线程环境下,这是一个错误的示例。这种方式在多线程下可能会产生多个实例,除非程序运行于单线程环境下,否则不要使用这种方式。但鉴于有很多其它实现方式,个人觉得如果严格要求单例,压根就不要使用这种方式。

接下来介绍的其它方法都是多线程安全的。另外,为了节省篇幅,代码就不加注释了。

方式2

既然说方式1在多线程环境下可能会产生多个类实例(因为多个线程可能会同时并发调用getInstance方法),那这种问题在多线程编程里面我们一般通过加锁的方式解决,也就是这里要说的方式2:

public class Singleton2 {
    private static Singleton2 instance;

    private Singleton2() {
    }

    public static synchronized Singleton2 getInstance() {
        if (instance == null) {
            instance = new Singleton2();
        }

        return instance;
    }
}

这种方式功能没问题,但每次调用getInstance都会去获取锁,如果调用频率比较高,就会产生性能问题。

方式3

对于方式2可能存在的性能问题,我们可以通过双重检查锁定模式(DCL,Double-Checked Locking)优化一下:

public class Singleton3 {
    private static volatile Singleton3 instance;

    private Singleton3() {
    }

    public static Singleton3 getInstance() {
        // 第一次检查
        if (instance == null) {
            synchronized (Singleton3.class) {
                // 第二次检查
                if (instance == null) {
                    instance = new Singleton3();
                }
            }
        }

        return instance;
    }
}

这样性能就会比方式2高很多了,因为第一次检查是没有加锁的,只有第二次检查才加锁了。而除了第一次初始化创建类实例的时候会进入到if代码块内进行加锁之外,其它时候都不会进入二次检查,也就不会加锁了。

另外需要注意的就是我们给实例字段加了volatile关键字修饰,主要是为了防止编译优化导致的顺序问题以及缓存不一致引发的bug(Java的内存模型允许还未完全初始化完全的对象就对外可见)。

虽然DCL的方式解决了性能问题,但它依旧有一些不好的地方:

  • 字段需要使用volatile关键字修饰
  • 代码有些冗余,需要我们自己使用锁的方式控制并发

那还有没有其它更好的方式呢?当然有,继续往下看。

方式4

public class Singleton4 {
    public static final Singleton4 INSTANCE = new Singleton4();

    private Singleton4() {
    }
}

这种方式直接在声明实例字段的时候就初始化它,而且我们将INSTANCE字段设置成public的了,所以就不需要静态工厂方法了。待会与方式5一起讨论。

方式5

public class Singleton5 {
    private static final Singleton5 INSTANCE = new Singleton5();

    private Singleton5() {
    }

    public static Singleton5 getInstance() {
        return INSTANCE;
    }
}

方式4和方式5非常相似,他们各自有各自的优点:

  • 方式4的优点主要有两个:1. 简单。 2. 将实例字段以static final的方式对外暴露,让使用者一看就知道该类是单例的,非常明确。
  • 方式5的优点主要有三个:1. 灵活。比如哪天你改变主意了,不想让该类是单例的了,直接修改getInstance方法的内部实现即可,使用者无需感知。2. 我们可以使用泛型实现一个通用的单例工程方法。3. 静态工厂类可以以方法引用(method reference)的方式作为supplier。意思就是可以这样写代码:

    Supplier<Singleton5> singleton5Supplier = Singleton5::getInstance;
    Singleton5 instance = singleton5Supplier.get();

那该如何选择呢?《Effective Java 3rd》一书中给出了讨论:对于方式4和方式5,如果上面方式5的优点你用不到,那就优先选方式4.

方式3中,我们自己使用锁来保证了单例,而方式4、5其实都是依赖了JVM对于静态字段和代码块依次有序初始化(Java Language Specification 12.4.2)来解决了多线程并发的问题,代码简单。但这种方式是不是就非常完美了呢?当然不是的!我们在类里面就实例化一个类,这样类在加载的时候就会去初始化,也就是没有达到惰性加载的目的。从刚才给的Java规范处我们知道类的初始化发生在第一次使用该类的字段或者方法,这样我们再优化一下,以达到惰性加载的目的。看方式6.

方式6

public class Singleton6 {
    private static class InstanceHolder {
        private static final Singleton6 INSTANCE = new Singleton6();
    }

    public static Singleton6 getInstance() {
        return InstanceHolder.INSTANCE;
    }
}

这里我们将类实例的创建封装在了内部类InstanceHolder中,这样,当我们第一次调用getInstance方法时,该类才会初始化。

当然,这种方式也并非完美无缺。其实之前的所有方法都存在序列化的问题:每次反序列化,都会产生一个新的实例。这样就不是单例了。而且只实现Serializable(implements Serializable)接口是不行的,还需要将所有实例字段声明为transient,并且提供一个readResolve方法:

private Object readResolve() {
    // Return the one true Singleton6 and let the garbage collector
    // take care of the Singleton6 impersonator.
    return InstanceHolder.INSTANCE;
}

原因参见《Effective Java 3rd》Item89.

当然还有另外一种创建单例的方式可以避免序列化这个问题,看方式7.

方式7

public enum EnumSingleton {
    INSTANCE;

    public EnumSingleton getInstance() {
        return INSTANCE;
    }
}

// 使用
// EnumSingleton enumSingleton = EnumSingleton.INSTANCE.getInstance();

这种实现单例的方式借助enum自身的实现机制保证了不会产生刚才说的序列化问题和多线程并发的问题,是目前综合来看最好的一种实现单例的方式。但也存在一些限制,比如无法继承非enum的类,但这种我们可以通过实现接口的方式来绕开。

以上就是常见的创建单例的7种方式。

单例模式存在的问题

单例模式的实质就是某种意义上的全局变量,我们知道在程序中应该尽量避免全局变量的使用,特别是对于可修改的全局变量,否则程序既难维护,也容易滋生各种Bug。有两种方式能改善这种情况:

  • 如果方法的确需要一个单例对象,能否通过参数传递的方式解决?这样一方面是灵活,比如可以传递不同的单例对象;另一方面容易通过Mock来做单元测试。
  • 如果我们真的需要实现单例(比如A类的单例),那可以额外提供一个工厂类(比如B类),然后由该工厂类B来保证A类的单例性,而不是A类自身。这样A类就是一个普通的类了。

另外,还有一些点可能需要注意:

  1. 单例指的是在一个JVM里面保证唯一,这样对于一些分布式系统,或者某些内部采用了分布式技术的系统来说可能会产生问题。
  2. 不同的class loader可能加载不同版本的单例。
  3. 如果没有对象引用该单例了,可能会被GC回收。等下次使用被再次创建的时候,可能已经和上次不完全一样了。

References:

]]>
0 http://niyanchun.com/singleton-in-java.html#comments http://niyanchun.com/feed/category/coding/
图解Java8新时间日期类 http://niyanchun.com/java-8-new-date-time-api.html http://niyanchun.com/java-8-new-date-time-api.html Sat, 18 May 2019 20:01:00 +0800 NYC Java 8引入新的时间日期API相比于原来的API(Date和Calendar)的好处:

  1. 线程安全
  2. 更易于理解和使用
  3. 增加了时区考虑

下面简单介绍。时间的处理说简单也简单,说难也难,这里引用《Java核心技术卷2》中的两段话:

Time flies like an arrow, and we can easily set a starting point and count forward and backward in seconds. So why is it so hard to deal with time? The problem is humans. All would be easy if we could just tell each other: “Meet me at 1523793600, and don’t be late!” But we want time to relate to daylight and the seasons. That’s where things get complicated.

Time zones, perhaps because they are an entirely human creation, are even messier than the complications caused by the earth’s irregular rotation. In a rational world, we’d all follow the clock in Greenwich, and some of us would eat our lunch at 02:00, others at 22:00. Our stomachs would figure it out. This is actually done in China, which spans four conventional time zones. Elsewhere, we have time zones with irregular and shifting boundaries and, to make matters worse, the daylight savings time.

看来问题复杂化的根因还是人。

新API中比较常用的日期/时间分两大类:

  • LocalDate/LocalTime/LocalDateTime:这三种分别表示日期、时间、时间+日期,不含时区信息
  • ZonedDateTime/OffsetDateTime:表示时间+日期,带时区信息

下面用两张图来解释一下一些问题。下面是一个ISO-8601标准格式的完整时间:

datetime

这个时间可以拆成几部分,分别对应Java里面的类:

  • LocalDate:2008-08-08
  • LocalTime:20:00:00
  • ZoneOffset:+08:00
  • ZoneId:Asia/Shanghai
  • LocalDateTime:2008-08-08T20:00:00
  • OffsetDateTime:2008-08-08T20:00:00+08:00
  • ZonedDateTime:2008-08-08T20:00:00+08:00 Asia/Shanghai

这个已经很清楚了,就不赘述了,更详细的信息可以查看各个类的JavaDoc。那带时区和不带时区的区别在哪里?看下面的图:

timeline

时光如流水,一去不复返,时间永远是向前的。人为规定UTC时间1970-01-01T00:00:00Z为Epoch时间,此时定义Instant的值为0秒,往后加,往前减。即Instant代表从Epoch至今所经过的秒数,精度可以到纳秒。Epoch时间之前的Instant值自然就是负数,之后是正数。另外定义两个Instant之间的差值为Duration(类似的还有一个Period,表示两个LocalDate之间的差值)。很显然,Instant描述的是一个时间点,而且是个“绝对时间”:即不管你在地球的哪个角落,只要Instant值一样,那就表示的是时间线中的同一个时间。

同理,带有时区信息的时间也描述的是一个绝对时间,比如时间线上面的品红色时间点,对于中国人民来说是“2008-08-08T20:00+08 Asia/Shanghai”,对于巴黎人民来说是“2008-08-08T14:00+02:00 Europe/Paris”,但对应到时间线上面其实是同一个时间。然而不带时区信息的本地时间描述的则不是一个绝对时间,比如中国人的“当地时间2020年1月1日20:00点”和巴黎人民的“当地时间2020年1月1日20:00点”放在时间线上不是同一个时间点。

那到底该使用本地时间还是带时区信息的时间呢?一般来说,当你不需要描述时间线上面一个绝对时间的时候都应该优先使用本地时间,比如生日、节假日、计算差值、周期等场景,因为有了时区以后,问题往往会变的比较复杂。反之,如果你需要描述一个绝对时间,则应该用带有时区信息的时间,或者是Instant。

所以,可以看到新的API中既有方便操作的本地时间,也有考虑了时区的时间类型,即文章开头提到的好处3。另外,这些新的类(实例)都是immutable的,即和String类一样,当需要产生一个新对象的时候,都是重新new了一个实例,而不会修改原有对象,所以都是线程安全的。关于易用性,直接看一些常用的代码片段吧:

public class Test {
  public static void main(String[] args) throws Exception {
    // 计算某个操作的耗时
    Instant start = Instant.now();
    Thread.sleep(2000);
    Instant end = Instant.now();
    System.out.println(Duration.between(start, end).toMillis());
    Instant.now().plus(1, ChronoUnit.DAYS);

    // LocalDate
    LocalDate today = LocalDate.now();
    System.out.println(today);
    LocalDate tomorrow = today.plusDays(1);
    System.out.println(tomorrow);

    // 类似于Instant,Period表示两个LocalDate之间的差值
    Period period = Period.between(today, tomorrow);
    System.out.println(period.getDays());

    // 常用构造函数
    LocalDateTime now = LocalDateTime.now();
    System.out.println(now);
    LocalDateTime ldt1 = LocalDateTime.of(LocalDate.now(), LocalTime.now());
    LocalDateTime ldt2 = LocalDateTime.of(2008, 8, 8, 20, 0, 0);
    LocalDateTime ldt3 = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());
    LocalDateTime ldt4 = LocalDateTime.parse("2008-08-08T20:00:00");
    LocalDateTime ldt5 = LocalDateTime.parse("2008-08-08 20:00:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

    // 常用操作
    LocalDateTime localDateTime = LocalDateTime.now();
    localDateTime.plus(1, ChronoUnit.DAYS);
    localDateTime.plusHours(1);
    localDateTime.minus(1, ChronoUnit.HALF_DAYS);
    localDateTime.minusSeconds(10);
    localDateTime.isBefore(LocalDateTime.parse("2008-08-08T20:00:00"));
    localDateTime.isAfter(LocalDateTime.parse("2008-08-08T20:00:00"));
    localDateTime.compareTo(LocalDateTime.parse("2008-08-08T20:00:00"));

    // 时间格式化——使用内置的一些格式
    DateTimeFormatter dtf = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
    System.out.println(dtf.format(now));
    // 时间格式化——自定义格式
    System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

    // 旧API向新API的转化,主要是通过Date和Calendar的toInstant()方法
    Date date = new Date();
    LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
    Calendar calendar = Calendar.getInstance();
    LocalDateTime.ofInstant(calendar.toInstant(), ZoneId.systemDefault());
  }
}

更多的使用到的时候Google,或者看JavaDoc即可。

总的来说:老API的功能新的全有,且更易用、更好用,所以没有任何理由继续使用旧的。

参考:

  • 《Core Java Volume II》
]]>
0 http://niyanchun.com/java-8-new-date-time-api.html#comments http://niyanchun.com/feed/category/coding/
Java Lambda表达式(下) http://niyanchun.com/java-lambda-part2.html http://niyanchun.com/java-lambda-part2.html Sun, 12 May 2019 22:52:00 +0800 NYC 上篇文章中我们总结了Lambda的一些基本知识,本文总结剩余的一个知识点:Lambda表达式中变量的作用域问题。

Lambda表达式变量作用域

这里使用书上面给的一个例子,我们定义了一个静态的repeatMessage方法,代码如下:

public static void repeatMessage(String text, int delay) {
    ActionListener listener = event -> {
        System.out.println(text);
        Toolkit.getDefaultToolkit().beep(); 
  };
    new Timer(delay, listener).start(); 
}

可以看到,这个方法里面有一个Lambda表达式,且表达式里面使用了外面方法的一个参数text。我们知道Lambda表达式经常使用的场景就是Lambda表达式所定义的逻辑在以后某个时间点可能才会执行。也就是说repeatMessage和里面的Lambda表达式未必是一起执行的,也许Lambda执行的时候repeatMessage已经执行过了,其参数肯定也早已经获取不到了,所以为了解决这个问题,Lambda表达式会事先将它所引用的外部的参数拷贝一份,一般称作Lambda表达式捕获(capture)了这个变量,而捕获的诸如text这种既不是Lambda表达式参数列表里面提供的变量,也不是Lambda body里面定义的变量,称之为自由变量(free variable)。所以这样看,一个Lambda表达式除了上文中介绍三部分外,还应该加上自由变量。

有了这个概念之后,我们先给出本文要讨论的问题的结论:Lambda表达式虽然是为了提供一种函数式编程,但它并没有创建一个新的变量作用域(scope),表达式中涉及的所有变量的作用域与定义Lambda的方法相同

说的有点绕,举个例子。比如上面的repeatMessage方法里面有一个Lambda表达式,表达式中使用了一个参数event,要注意这个event变量的作用域是整个repeatMessage方法,而不仅仅是Lambda表达式。也就是说,如果在repeatMessage方法里面的其它地方再定义一个event,那就会变量名冲突。这就是并没有创建一个新的变量作用域的意思。这里我们再举几个例子:

@FunctionalInterface
interface RepeatMessage {
    void repeatMessage(String message, int times);
}

public class LambdaTest {

    public static void main(String[] args) throws Exception {
        int times, i;
        RepeatMessage repeatMessage = (msg, times) -> {
            for (int i = 0; i < times; i++) {
                System.out.println(msg);
            }
        }
    }
}

上面代码中有两处错误:1. Lambda表达式的参数列表中定义了一个变量times,这个与上面的int times变量冲突了。2. Lambda表达式里面的循环中定义了一个变量i,这个与外面定义的i也冲突了。所以只要记住Lambda表达式并没有创建一个新的变量作用域即可。书中的原话是:

The body of a lambda expression has the same scope as a nested block.

有效的final (effectively final)

Java对于Lambda中捕获的自由变量有一个非常重要的限定:该变量必须是effectively final的。所谓effectively final简单理解就是这个变量一旦初始化之后就不能再重新赋值了(An effectively final variable is a variable that is never assigned a new value after it has been initialized.)。这样限定的主要原因是Lambda表达式可能会并发执行,在里面修改捕获的变量的值可能会产生问题。

其实这个"effectively final"概念在Java其它地方也有用到,所以为了更准确的理解这个概念,我专门查了一下相关文档,Lambda Specification, Part B: Lambda Expressions4.12.4 Final Variables节有稍微细致一点的说明,内容不多,我就全部搬过来做简单说明。

场景1

Certain variables that are not declared final may instead be considered effectively final.

A local variable or a method, constructor, lambda, or exception parameter is effectively final if it is not final but it never occurs as the left hand operand of an assignment operator (15.26) or as the operand of a prefix or postfix increment or decrement operator (15.14, 15.15).

上面定义了这样一种场景:局部变量、方法的参数、构造器的参数、Lambda的参数、异常的参数等虽然没有被final修饰,但也没有作为复制操作符的左值(简单说就是没有被赋值),并且也没有使用++--操作符。在这种情况下,即使变量没有被final修饰,我们也可以认为它是有效的final变量(effectively final)。这个比较好理解,因为排除这几种情况的话,变量是不可能有途径改变的,自然就是final的了。

场景2

除了上面这种情况外,还定义了另外一种场景:

In addition, a local variable whose declaration lacks an initializer is effectively final if all of the following are true:

  • It is not final.
  • Whenever it occurs as the left-hand operand of an assignment operator, it is definitely unassigned and not definitely assigned before the assignment (that is, it is definitely unassigned and not definitely assigned after the right-hand operand of the assignment) (16).
  • It never occurs as the operand of a prefix or postfix increment or decrement operator.

要特别注意,这个场景只适用于局部变量,而上面规定的那种场景适用于局部变量、方法、构造器、Lambda、异常的参数等多种情况。这个场景简单解释就是:对于那种声明时没有初始化的局部变量,如果同时满足三个条件,那它也是有效的final:1. 没有被final修饰。2. 该变量之前一定没有被赋过值。这个条件主要是要限定对于一个之前只声明过的变量,那它后面如果只被明确的(definitely)一次值,也是可以的。3. 没有使用++操作符。

最后看一些例子:

Examples of effectively final:

void m1(int x) {
   int y = 1;
   foo(() -> x+y);
   // Legal: x and y are both effectively final.
   // 这个好理解,x没有被赋值过,y只被赋值过一次。
 }

 void m2(int x) {
   int y;
   y = 1;
   foo(() -> x+y);
   // Legal: x and y are both effectively final.
   // 这个也好理解,x没有被赋值过,y第一次只声明了,后面只被赋值过一次。
 }

 void m3(int x) {
   int y;
   if (..) y = 1;
   foo(() -> x+y);
   // Illegal: y is effectively final, but not definitely assigned.
   // 这里y不是有效的final的原因主要是不符合"明确的赋值(definitely assigned)"的限定,因为如果if不成立,y就没有被赋值;if成立了,y又被赋值了。
 }

 void m4(int x) {
   int y;
   if (..) y = 1;
   else y = 2;
   foo(() -> x+y);
   // Legal: x and y are both effectively final.
   // 这里y声明之后,肯定会被赋值,且只会被赋一次值(不是在if中,就是在else中)
 }

 void m5(int x) {
   int y;
   if (..) y = 1;
   y = 2;
   foo(() -> x+y);
   // Illegal: y is not effectively final.
   // 这里y可能会被赋两次值
 }

 void m6(int x) {
   foo(() -> x+1);
   x++;
   // Illegal: x is not effectively final.
   // 这里x使用了自加操作符
 }

 void m7(int x) {
   foo(() -> x=1);
   // Illegal: x is not effectively final.
   // 这里x是方法的参数,不是局部变量,所以只能用场景1来评判。而x用作了赋值操作符的左值,所以不是有效的final变量。
 }

 void m8() {
   int y;
   foo(() -> y=1);
   // Illegal: y is not definitely assigned before the lambda (see 15.27.2)
   // 这个稍微难理解一点,之前有说过,Lambda捕获的自由变量一定要是有效的final,而y之前只声明了,并没有只确切的初始化过一次,所以肯定不是有效的final。
 }

void m9(String[] arr) {
  for (String s : arr) {
    foo(() -> s);
    // Legal: s is effectively final (it is a new variable on each iteration)
  }
}

void m10(String[] arr) {
  for (int i = 0; i < arr.length; i++) {
    foo(() -> arr[i]);
    // Illegal: i is not effectively final (it is incremented)
    // 这里i使用了自加操作符
  }
}

最后,还有一段非常重要的话:

If a variable is effectively final, adding the final modifier to its declaration will not introduce any compile-time errors. Conversely, a local variable or parameter that is declared final in a valid program becomes effectively final if the final modifier is removed.

这个对于我们判断变量是不是有效的final非常有用:如果是有效的final,那显式的加上final关键字肯定不会产生错误。相反的,对于本身已经被final修饰的变量,并且没有任何错误的话,那去掉final修饰符,这个变量肯定也是有效的final

Reference

  • Core Java Volume I
]]>
0 http://niyanchun.com/java-lambda-part2.html#comments http://niyanchun.com/feed/category/coding/
Java Lambda表达式(上) http://niyanchun.com/java-lambda-part1.html http://niyanchun.com/java-lambda-part1.html Sat, 11 May 2019 17:35:00 +0800 NYC Java 8中引入了Lambda表达式,目的是支持函数式编程。为了说明问题假设我们有一个需求:使用List的forEach方法遍历输出一个List。

先说明两个点:

  • 当然遍历List有很多种方式,这里限定使用forEach只是为了说明Lambda;
  • 其实forEach和Lambda一样都是Java 8才引入的,但不影响问题说明。

旧方式:在没有Lambda之前,我们的代码类似下面这样:

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6);

list.forEach(new Consumer<Integer>() {
    @Override
    public void accept(Integer integer) {
        System.out.println(integer);
    }
});

新方式1:有了Lambda之后,我们的代码可以简化成下面这样:

list.forEach(n -> System.out.println(n));

新方式2:甚至再简单一点:

list.forEach(System.out::println);

因为Java是纯OOP语言,没有函数这种概念,所以当需要传递一段功能性代码逻辑的时候,我们必须像上面老式方法中的那样,定义一个匿名类,然后在类中实现接口,这样做比较麻烦,而且代码很冗余。有了Lambda之后,我们就无需定义一个类了,直接写功能性逻辑就行,代码非常精简,这也就是所谓的函数式编程,非常类似于其它语言里面的闭包、匿名函数等概念。宏观感受之后,接下来我们看下Lambda的一些细节。

函数式接口(Functional Interface)

所以我们先看函数式接口。先看上面例子中forEach方法的参数Consumer<? super T> action

// package java.util.function;
@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);

    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

限于篇幅,我把代码中所有的注释都删掉了。对于这个参数我们需要关注下面两个点:

  • 它使用了@FunctionalInterface注解;
  • 它只有一个抽象方法accept()

没错,Consumer就是一个函数式接口(Functional Interface),这个我们在前面介绍接口的文章里面已经介绍过了,这里看下JSR-000335 Lambda Expressions for the JavaTM Programming LanguagePart A: Functional Interfaces中是如何定义的:

A functional interface is an interface that has just one abstract method (aside from the methods of Object), and thus represents a single function contract. (In some cases, this "single" method may take the form of multiple abstract methods with override-equivalent signatures inherited from superinterfaces; in this case, the inherited methods logically represent a single method.)

原文不是很长,有兴趣的可以看一下。简单概括函数式接口指只包含一个抽象方法的接口。而且Java 8中增加了一个注解@FunctionalInterface来表名接口是函数式接口,但这个注解并非强制性的。但如果我们准备将一个接口定义为函数式接口,那最好加上这个注解,这样一旦接口不符合函数式接口的条件的时候,编译器就会报错。如果不加这个注解,编译器是不知道的。

Lambda表达式就是为函数式接口而生的,凡是参数是函数式接口的方法,在调用的时候,都可以传递一个Lambda表达式作为参数。就像上面的forEach例子一样。

知道了什么时候(When)用Lambda表达式,我们再来看如何(How)用Lambda表达式。

Lambda表达式语法

一个Lambda表达式(Lambda Expression)由三部分组成:

  • 使用圆括号括起来的参数列表,
  • ->符号,
  • 表达式。

一个例子(按照字符串长短排序):

String[] strArray = new String[]{"def", "cbad", "na", "a"};
Arrays.sort(strArray, (String a, String b) -> a.length() - b.length());

语法就不细介绍了,只列举一些注意点:

  1. 对于参数列表:

    • 如果参数的类型可以推断出来的话,那可以省略参数的类型。比如上面新方式1中forEach例子中的n,我们没有写类型,因为编译器可以根据list对象推断出来n的类型为Integer;
    • 如果只有一个参数,且类型可以推断出来,则可以省略掉圆括号;
    • 如果一个参数也没有的话,必须用一对空括号占位。
  2. 对于表达式:

    • Lambda表达式中,我们永远不需要指定返回值的类型,由编译器自己推断;
    • 如果表达式比较复杂,超过了一个表达式,我们可以使用一对大括号{},然后在大括号里面写多个表达式。
    • 单个表达式的时候,我们无需显式的加return语句,由编译器自己推断返回值。但使用了大括号之后,如果有返回值,我们需要自己显式的加上return语句。

方法引用(Method References)

上面forEach例子中的新方式2就是方法引用的例子。Lambda表达式中,我们自定义了代码逻辑,但如果我们想要实现的逻辑已经被某个方法实现了的话,我们可以直接引用,这就是所谓的方法引用。比如上面forEach例子中我们想要实现的就是打印元素,这个System.out.println方法已经实现了,所以我们可以直接引用,而无需自己去实现表达式。方法引用有三种格式:

  • 对象::实例方法(object::instanceMethod)
  • 类名::静态方法(Class::staticMethod)
  • 类名::实例方法(Class::instanceMethod)

前两种其实只是Lambda表达式的简写,比如:

  • 第一种System.out::println等价于x->System.out.println(x),
  • 第二种Math::pow等价于(x, y) -> Math.pow(x,y),
  • 第三种特殊一些,第一个参数会变为方法的目标(the target of the method),即方法的调用者,比如String::compareToIgnoreCase等价于(x, y) -> x.compareToIgnoreCase(y).

我们还可以在方法引用中使用this关键字。比如:this::equal等价于x -> this.equal(x)

还可以使用super关键字用来调用方法所在类的父类实例方法,语法为:super::instanceMethod。例如:

class Greeter {
    public void greet() {
        System.out.println("Hello, world!"); 
    }
}

class TimedGreeter extends Greeter {
    public void greet() {
        Timer t = new Timer(1000, super::greet);
        t.start(); 
    }
}

构造器引用(Constructor References)

构造器引用和方法引用类似,只不过把方法名换成了new关键字。因为引用构造器就是为了new对象,至于new的时候调用类的哪个构造器,则根据上下文推断。比如int[]::new等价于x -> new int[x].

这里我们举一个非常实用的例子做说明:在Java 8+中如何将一个Map根据Key或者Value排序?代码如下:

// 构造Map
final Map<String, Integer> unSortedMap = new HashMap<>();
    unSortedMap.put("key1", 10);
    unSortedMap.put("key2", 5);
    unSortedMap.put("key3", 8);
    unSortedMap.put("key4", 20);


// 按照Key,升序排
final Map<String, Integer> sortedByKeyAsc = unSortedMap.entrySet()
        .stream()
        .sorted(Map.Entry.comparingByKey())
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (oldValue, newValue) -> oldValue, LinkedHashMap::new));

// 按照Key,降序排
final Map<String, Integer> sortedByKeyDesc = unSortedMap.entrySet()
        .stream()
        .sorted(Map.Entry.<String, Integer>comparingByKey().reversed())
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (oldValue, newValue) -> oldValue, LinkedHashMap::new));

// 按照Value,升序排
final Map<String, Integer> sortedByValueAsc = unSortedMap.entrySet()
        .stream()
        .sorted(Map.Entry.comparingByValue())
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (oldValue, newValue) -> oldValue, LinkedHashMap::new));

// 按照Value,降序排
final Map<String, Integer> sortedByValueDesc = unSortedMap.entrySet()
        .stream()
        .sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (oldValue, newValue) -> oldValue, LinkedHashMap::new));

这种排序方式代码非常简洁,且可读性高。这里简单说明一下其中的sortedcollect

sorted用于指定比较规则,只有一个参数Comparator<T>,这个参数呢是给函数式接口,其定义的抽象方法为int compare(T o1, T o2);,代码中的comparingByKeycomparingByValue方法都实现了这个接口。当然我们自己也可以实现这个接口,这样就可以按照任意字段排序了。想一下,如果我们把上面的Map的Value由Integer换成一个Map<String, Integer>类型,即现在是一个Map的Map。现在我们需要根据里层Map的一个整数字段排序,如何实现?比如现在的Map长下面这样:

final Map<String, Map<String, Integer>> unSortedNestedMap = new HashMap<>();
final Map<String, Integer> innerMap1 = new HashMap<>();
innerMap1.put("count", 10);
innerMap1.put("size", 100);
final Map<String, Integer> innerMap2 = new HashMap<>();
innerMap2.put("count", 5);
innerMap2.put("size", 200);
final Map<String, Integer> innerMap3 = new HashMap<>();
innerMap3.put("count", 15);
innerMap3.put("size", 50);

unSortedNestedMap.put("innerMap1", innerMap1);
unSortedNestedMap.put("innerMap2", innerMap2);
unSortedNestedMap.put("innerMap3", innerMap3);

现在如何对根据里层Map的count的值对unSortedNestedMap进行排序?代码如下:

final Map<String, Map<String, Integer>> sortedNestedMapByInnerMapCount = unSortedNestedMap.entrySet()
    .stream()
    .sorted((e1, e2) -> e2.getValue().get("count").compareTo(e1.getValue().get("count")))
    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (oldValue, newValue) -> oldValue, LinkedHashMap::new));

其实代码也很简单,和上面的单层Map排序没有实质区别,只是我们自己实现了一下sorted那个函数接口参数而已。

另外一个就是collect,这个方法有一个参数Collector<T, A, R>,也是一个接口,但该接口并不是一个函数式接口,但其定义的几个抽象方法的返回值都是函数式接口类型的。我们使用Collectors.toMap方法可以产生一个Collector<T, A, R>类型的参数。Collectors.toMap方法有四个参数,而这四个参数均是函数式接口类型的:

public static <T,K,U,M extends Map<K,U>> Collector<T,?,M> toMap(Function<? super T,? extends K> keyMapper,
                                                                Function<? super T,? extends U> valueMapper,
                                                                BinaryOperator<U> mergeFunction,
                                                                Supplier<M> mapSupplier)                                                                                                                     

类型参数和参数含义说明如下:

Type Parameters:

  • T - the type of the input elements
  • K - the output type of the key mapping function
  • U - the output type of the value mapping function
  • M - the type of the resulting Map

Parameters:

  • keyMapper - a mapping function to produce keys
  • valueMapper - a mapping function to produce values
  • mergeFunction - a merge function, used to resolve collisions between values associated with the same key, as supplied to Map.merge(Object, Object, BiFunction)
  • mapSupplier - a function which returns a new, empty Map into which the results will be inserted

在给这个方法传参时我们把Lambda三种形式都使用到了:

  • 表达式:(oldValue, newValue) -> oldValue
  • 方法引用:Map.Entry::getKey和Map.Entry::getValue
  • 构造器引用:LinkedHashMap::new。这里为了保证顺序,我们使用了LinkedHashMap

内置的函数式接口

Java内置了一些标准的非常通用的函数式接口,一般我们如果需要使用函数式接口的话,应该优先使用内置的里面选择;如果没有满足需求的,那就自己定义,但最好加上@FunctionalInterface注解。

内置的函数式接口主要分为两类:一类输入/输出是引用类型的,另一类是输入/输出是原始类型的。优先使用后者,因为可以避免自动装箱和拆箱。

注:以下图片截自《Core Java Volumn I - Fundamentals》。

引用类型:
引用类型

原始类型:
原始类型

因为篇幅原因,Lambda最后的一个重要点“变量作用域”下篇文章再介绍。

Reference

  • Core Java Volumn I
]]>
0 http://niyanchun.com/java-lambda-part1.html#comments http://niyanchun.com/feed/category/coding/
Java 8接口新特性 http://niyanchun.com/java-interface.html http://niyanchun.com/java-interface.html Sat, 04 May 2019 21:48:00 +0800 NYC 定义
Interfaces, is a way of describing what classes should do, without specifying how they should do it.

和大多数有接口这项技术的语言里面一样,接口只负责定义行为(方法),具体行为的实现由类完成。

另外要说明的就是Java 8(Java SE 8/JDK 1.8)引入的函数式接口(functional interface):只包含一个抽象方法的接口。这里要注意有两个条件:只包含一个,且是抽象的方法(所以只限定了抽象方法,可以包含多个非抽象方法)。函数式接口可以选择性使用@FunctionalInterface注解修饰,如果使用了该注解,但接口又不符合条件,编译的时候编译器就会报错。函数式接口主要是配合lambda表达式使用的(以后再讨论)。

一些特性

  1. 实现一个接口使用implements关键字;实现接口的类必须实现接口声明的所有抽象方法(当然如果是抽象类的话,可以只定义不实现)。
  2. 不能new一个接口实例;但可以声明一个接口类型的变量;接口类型的变量只能引用实现了该接口的类的对象。我们可以使用instanceof关键字检查一个对象是否实现了某个接口。

    // 错误,List是一个接口,不能new一个接口实例
    X = new List<String>;        
    
    // OK,可以声明一个接口类型的变量
    List<String> stringList;        
    
    // OK,ArrayList实现了List接口
    List<String> strList = new ArrayList<>(1);
    
    // 错误,HashMap没有实现List接口
    List<String> strList = new HashMap<String, String>();
    
    // 使用instanceof关键字检测一个对象是否实现了一个接口
    if (strList instanceof Map) {
            
    }
  3. 接口之间可以继承,比如A接口可以继承B、C接口。
  4. 接口内的方法默认都是public的,字段默认是public static final的,所以接口内只能定义方法和常量。
  5. 类只能继承一个类,但可以实现多个方法。Java为了简单,不像C++那样允许多继承,这也是引入接口这项技术的一个主要原因。

静态方法

从Java 8开始,接口里面可以定义静态方法了(static methods)。这个主要会涉及一个编程习惯的改变。比如在Java 8之前,为了OOP,定义一个接口的同时,会定义一个对应的类(这个类一般是抽象的或者不允许继承的),然后在接口中声明方法,在抽象类中定义一些静态方法。标准库中就有许多类似的技术,比如Path是一个接口,声明了一些方法,与之对应的是一个Paths类,该类中定义了一些静态方法:

// Path接口
public interface Path
    extends Comparable<Path>, Iterable<Path>, Watchable
{
    ...
    FileSystem getFileSystem();
    ...     
}

// 对一个的Paths类
public final class Paths {
    
    ...
    public static Path get(String first, String... more) {
        return FileSystems.getDefault().getPath(first, more);
    }
    ...

}

但从Java 8开始,如果有该类需求,无需再额外定义一个类了,直接在接口中实现静态方法即可(这种方式也是比较推荐的)。比如上面的Paths类实现的功能可以挪到Path接口里面去:

public interface Path
    extends Comparable<Path>, Iterable<Path>, Watchable
{
    ...
    FileSystem getFileSystem();
    ...     
    
    public static Path get(String first, String... more) {
        return FileSystems.getDefault().getPath(first, more);
    }
}

当然,JDK中的标准库因为数量庞大不太会重构了。

默认方法

Java 8开始,可以给接口中的方法提供一个默认的实现,实现方式是在方法前添加default修饰符,并给方法提供一个方法体。这样如果实现该接口的类覆写(override)了该方法则以类中的实现为准,如果没有则使用接口中默认的实现。而且默认方法可以调用接口中的所有其它的方法(包括抽象方法)。

这种技术有一个非常重要的用途就是“接口进化(interface evolution)”,比如标准库中的Collection接口已经存在很多年了(since 1.2),已经有很多类实现了该方法。如果现在我们在该接口中增加几个方法,那原来的那些类就会编译不过了,因为它们没有实现新增的方法。如果我们不重新编译原来那些类,使用旧的Jar包,那如果有新的类使用了新增的方法,那又会报AbstractMethodError错误。这个时候默认方法就非常有用了,我们把新增的方法全部定义为默认方法,这样原来的类也不用必须实现这些方法,新的类可以覆写这些方法,两边都不影响。标准库也的确是这样做的,比如Collection接口在1.8中引入的新的方法都是定义成默认方法的:

...
default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
}

default Stream<E> parallelStream() {
    return StreamSupport.stream(spliterator(), true);
}
...

最后要讨论的就是相同的默认方法冲突的问题:如果相同的默认方法同时出现在了实现接口的类里面或者多个接口里面,会发生什么?

这里要注意:

  • 方法相同指的是方法名和参数都相同。
  • 这里说的是默认方法,而非常规的抽象方法,如果都是抽象方法,因为没有具体的实现,即使接口和类里面都有,抑或是多个接口里面都有,也是不存在任何冲突的。

然后我们看Java里面有默认方法相同时的冲突处理规则:

  1. 类优先原则。即如果是类和接口冲突了,那以类里面的实现为准,接口里面的默认方法就被自动忽略了。
  2. 如果多个接口里面有相同的方法,且其中有至少一个方法是默认方法,那编译就会报错。程序员必须在实现接口的类中通过重写方法来解决冲突。

第1个规则很好理解,第2个举个例子。比如类A同时实现了M和N两个接口,而M和N中呢都有一个方法String getName(),这个时候就要分情况了:

  • 如果M和N中的getName方法都是抽象的,那就像前面说的,不会有任何冲突;
  • 如果M和N中至少有一个getName方法是默认方法,那编译就会报错,此时,程序员必须在类A中覆写getName方法来解决冲突。比如可以自己实现,也可以使用M.super.getName()的方式来显式的说明使用哪个接口的默认方法。
]]>
0 http://niyanchun.com/java-interface.html#comments http://niyanchun.com/feed/category/coding/
Java的异常 http://niyanchun.com/java-exception.html http://niyanchun.com/java-exception.html Tue, 30 Apr 2019 21:29:00 +0800 NYC 异常分类

先上图:

Java Exception

Java的顶级异常类是Throwable类,下面分为ErrorException两大子类。Error及其子异常代表的是Java运行时系统内部错误,资源耗尽等情况。如果这种异常发生了,我们只能让自己的程序退出。而Exception及其分支异常则是我们写代码时需要关注的。

上面是按照类的继承来分的。另外一种更重要的分法是把ErrorRuntimeException及其子类异常称为非受检异常(unchecked exception),剩余其它的所有异常称为受检异常(checked exception)。所谓非受检异常就是我们代码中无需对这类异常进行捕获处理,但受检异常则必须有地方捕获处理,否则编译器编译的时候就会报错。

为什么非受检异常无需处理呢?一方面,对于Error类的异常刚才说了,是系统内部错误,任何时候任何地方都可能出现,我们避免不了,如果出现了,我们也无能为力。另一方面,对于RuntimeException异常,基本都是因为我们的代码有bug导致的,这个时候正确的解决方法是修复代码bug,而不是try catch(如果你都已经知道catch了,那你还不赶紧改掉bug?)。

为什么受检异常需要程序处理?因为受检异常也无法避免,且往往不是我们自己代码bug导致的(这点和RuntimeException不同),但如果出现了这类异常,我们一般还是可以处理的(这点和Error不同)。拿受检异常FileNotFoundException举个例子,我们从磁盘读取文件时,如果文件不存在就会抛这个异常,系统上面这个文件存不存在不是我们能决定的,也许你可以先判断存不存在,存在才去读。但这两个操作不是一个原子操作,也许你检查的时候在,真正去读的时候就不在了,所以避免不了。但如果真的出现了,我们的程序不一定就要退出,也许可以采取其它方式让程序继续,这是由业务场景决定的。比如比较常见的一种场景就是配置文件,如果不存在,就全部使用默认值。

以下是Error的直接子类:

AnnotationFormatError, AssertionError, AWTError, CoderMalfunctionError, FactoryConfigurationError, FactoryConfigurationError, IOError, LinkageError, SchemaFactoryConfigurationError, ServiceConfigurationError, ThreadDeath, TransformerFactoryConfigurationError, VirtualMachineError

以下是Exception的直接子类:

AclNotFoundException, ActivationException, AlreadyBoundException, ApplicationException, AWTException, BackingStoreException, BadAttributeValueExpException, BadBinaryOpValueExpException, BadLocationException, BadStringOperationException, BrokenBarrierException, CertificateException, CloneNotSupportedException, DataFormatException, DatatypeConfigurationException, DestroyFailedException, ExecutionException, ExpandVetoException, FontFormatException, GeneralSecurityException, GSSException, IllegalClassFormatException, InterruptedException, IntrospectionException, InvalidApplicationException, InvalidMidiDataException, InvalidPreferencesFormatException, InvalidTargetObjectTypeException, IOException, JAXBException, JMException, KeySelectorException, LambdaConversionException, LastOwnerException, LineUnavailableException, MarshalException, MidiUnavailableException, MimeTypeParseException, MimeTypeParseException, NamingException, NoninvertibleTransformException, NotBoundException, NotOwnerException, ParseException, ParserConfigurationException, PrinterException, PrintException, PrivilegedActionException, PropertyVetoException, ReflectiveOperationException, RefreshFailedException, RemarshalException, RuntimeException, SAXException, ScriptException, ServerNotActiveException, SOAPException, SQLException, TimeoutException, TooManyListenersException, TransformerException, TransformException, UnmodifiableClassException, UnsupportedAudioFileException, UnsupportedCallbackException, UnsupportedFlavorException, UnsupportedLookAndFeelException, URIReferenceException, URISyntaxException, UserException, XAException, XMLParseException, XMLSignatureException, XMLStreamException, XPathException

以下是RuntimeException的直接子类:

AnnotationTypeMismatchException, ArithmeticException, ArrayStoreException, BufferOverflowException, BufferUnderflowException, CannotRedoException, CannotUndoException, ClassCastException, CMMException, CompletionException, ConcurrentModificationException, DataBindingException, DateTimeException, DOMException, EmptyStackException, EnumConstantNotPresentException, EventException, FileSystemAlreadyExistsException, FileSystemNotFoundException, IllegalArgumentException, IllegalMonitorStateException, IllegalPathStateException, IllegalStateException, IllformedLocaleException, ImagingOpException, IncompleteAnnotationException, IndexOutOfBoundsException, JMRuntimeException, LSException, MalformedParameterizedTypeException, MalformedParametersException, MirroredTypesException, MissingResourceException, NegativeArraySizeException, NoSuchElementException, NoSuchMechanismException, NullPointerException, ProfileDataException, ProviderException, ProviderNotFoundException, RasterFormatException, RejectedExecutionException, SecurityException, SystemException, TypeConstraintException, TypeNotPresentException, UncheckedIOException, UndeclaredThrowableException, UnknownEntityException, UnmodifiableSetException, UnsupportedOperationException, WebServiceException, WrongMethodTypeException

本节最后再说明一个问题:能否用try...catch来捕获非受检异常,比如空指针异常NullPointerException?

答案是可以的,任何异常都可以用try...catch来捕获,不会有语法错误,且如果发生了对应的异常,的确可以捕获到。但语法上可以不代表实际中就能这么用,实际编码的时候千万别去捕获非受检异常,那样会被别人鄙视的。如果你已经预见可能会产生某种RuntimeException的异常,在代码里面做一下判断就好了,而不是用try...catch.

关于异常使用的一些注意点

  1. 捕获多个异常时,后面的异常不能是前面异常的子类,否则会报语法错误。比如下面的就是错误的,因为后面的FileNotFoundException是前面IOException的子类:

    try {
        // do something;
    } catch (IOException ioe) {
        // do something;
    } catch (FileNotFoundException e) {
        // do something;
    }
  2. 从Java SE7开始,可以在一个catch里面捕获多个异常,但要注意多个异常之间不能有继承关系,比如不能在一个catch里面捕获FileNotFoundExceptionIOException,因为他两个有继承关系。使用语法为:catch (ExceptionType1 | ExceptionType2 e). 还有要特别注意的是,捕获多个异常的时候,我们定义的那个异常变量e是final的,所以不能在catch的函数体里面给它赋值。但如果是只捕获一个异常的情况下,不是final,可以赋值。看下面代码:

    try {
        // do something;
    } catch (FileNotFoundException e1) {
        // 没有问题,e1不是final,可以重新赋值
        e1 = new FileNotFoundException("xxx");      
    } catch (IOException | ParseException e2) {
        // 语法错误,e2是final的,不能赋值
        e2 = new IOException("xxx");
    }
  3. 我们覆写(override)父类的方法的时候,如果这个方法有抛出受检异常,那我们覆写后抛出的异常不能比父类抛出的异常更宽泛,也就是说只能抛出父类抛出的异常或者其子类异常。当然,父类抛异常,子类覆写后不抛异常也是完全OK的。
  4. 我们用throws声明抛出一个受检异常,实际抛的时候,抛出声明的异常的子类异常也是OK的。捕获到异常的人可以使用e.getClass().getTypeName()获取抛出的真正异常类别。

使用异常的一些技巧

最后附一下Core Java Volume I一书中关于使用异常的一些技巧,为了保持原汁原味就直接附上英文了。

  1. Exception handling is not supposed to replace a simple test. 这个意思就是不要把try...catch当成分支判断去使用。

    // 假设我们需要遍历一个栈的元素,如何处理栈空的情况呢?
    
    // 正确的方式是每次先判断栈是否空,如下:
    if (!s.empty()) { s.pop(); }
    
    // 错误的方式是捕获栈空时抛出的异常(这是一个非受检异常),如下
    try {
        s.pop();
    } catch (EmptyStackException e) {
        // do something
    }
  2. Do not micromanage exceptions. 就是别写太多小的try...catch。如下是一个错误的示例(正确的方式是把for循环里面的两个小try...catch合并为一个大的try...catch):

    PrintStream out;
    Stack s;
    for (i = 0; i < 100; i++) {
        try {
            n = s.pop();
        } catch (EmptyStackException e) {
            // stack was empty
        }
    
        try {
            out.writeInt(n);
        } catch (IOException e) {
            // problem writing to file
        }
    }
  3. Make good use of the exception hierarchy. 意思就是要合理使用异常的层次结构,尽量不要直接抛顶级异常,比如Throwable、Exception、RuntimeException等,尽量找一个比较合适的子类异常。如果没有,可以自定义异常。
  4. Do not squelch exceptions. 不要压制异常。
  5. When you detect an error, “tough love” works better than indulgence. 当你发现错误时,“强硬的爱”比放纵更有效。
  6. Propagating exceptions is not a sign of shame. 把异常抛出去并不可耻。

4、5、6三条要表达的核心意思就是该出手时就出手,该抛异常就抛异常。别觉得这个异常发生的几率很低,就自己默默地捕获然后忽略了,或者没有正确处理。把异常抛给更适合处理异常的上层代码并没有什么不合适的。早点抛出,晚点捕获(throw early, catch late)。

Reference

  • Core Java Volume I
]]>
0 http://niyanchun.com/java-exception.html#comments http://niyanchun.com/feed/category/coding/
OOP中static的原罪 http://niyanchun.com/use-static-in-oop-carefully.html http://niyanchun.com/use-static-in-oop-carefully.html Sun, 21 Apr 2019 21:10:00 +0800 NYC 最近项目中使用Spring Boot,让我稍微有些水土不服,主要原因就是static。看下面两种代码风格:

风格1:

@Autowired
MyService myservice;

// doSomething是MyService类的非静态方法
Response resp = myservice.doSomething(someParameters);

风格2:

// doSomething是MyService类的静态方法
Response resp = MyService.doSomething(someParameters);

你平时Coding喜欢哪一种?对我而言,如果doSomething是无状态的,我一般都是使用第二种。更广一点,对于任何我状态的,我一般都喜欢写成static的。主要原因是调用起来方便,使用者不需要创建实例即可使用。而且一般静态调用会比实例调用快一些,因为没有实例创建、初始化的动作。但在使用Spring时,这种习惯让我举步维艰。因为Spring的核心技术之一就是依赖注入(Dependency Injection,DI),而注入的变量其实都是各个Bean的非静态类实例,而我们都知道,在静态方法里面是无法使用非静态变量的。如果要使用这些注入的对象,最简单的方式就是把使用的这些方法都写成非静态的。这多少让我有些不适应,特别是如果这个方法是无状态的,甚至它只是个偏工具的方法或者类。直觉告诉我,对于一个成熟的东西,如果你用着别扭,那很大可能是因为你的用法不对。于是我尝试着Goole了一下,发现和我有同样问题的人还不在少数,Stack Overflow上面也有许多牛人给的workaround,但终归只是workaround,还是让人觉得用着变扭。再深究,发现其实在OOP里面就不应该用static,至少要少用、慎用。这便是static的原罪:使用static方法这种编程属于procedural programming(面向过程的程序设计),而不是OOP。

先来看个例子,假设我们有一个Person类和一个PersonApp类如下:

@Getter
@Setter
public class Person {
    private int id;
    private String firstName;
    private String lastName;

    public static Person createNew() {
        Person temp = new Person();
        temp.id = -1;
        temp.firstName = "";
        temp.lastName = "";

        return temp;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                '}';
    }
}


public class PersonApp {
    public void doSomething() {
        // 调用静态方法createNew()
        Person person = Person.createNew();
        person.setId(1);
        person.setFirstName("Yanchun");
        person.setLastName("Ni");

        // 调用实例方法toString()
        System.out.println(person.toString());
    }
}

目前来看,PersonApp可以很方便的使用Person中的方法,不论是静态方法createNew()还是非静态方法toString()。但随着业务发展呢,我们又接到一个新的需求,需要增加一个员工类Employee,并且是从Person继承来的。然后我们定义了如下员工类:

@Getter
@Setter
public class Employee extends Person {
    private String boss;
    private String department;

    @Override
    public String toString() {
        return "Employee{" +
                "boss='" + boss + '\'' +
                ", department='" + department + '\'' +
                ", FirstName()='" + super.getFirstName() + '\'' +
                ", FirstName()='" + super.getLastName() + '\'' +
                '}';
    }
}

可以看到,对于非静态的toString()方法,我们可以通过覆写父类的toString()来实现自己的toString()。和Person类一样,我们也需要提供一个createNew()来创建Employee实例,但遗憾的是我们无法覆盖一个静态方法,只能重新写一个createNew()。显然覆盖写是OOP中的编程思想,而重新写一个是面向过程里面的思想。这只是一个继承的例子。OOP中还有很多技术都依赖类实例实现,比如多态、注入、基于接口的编程(interface-driven)、基于代理的AOP等。而DI、AOP都是Spring使用的核心技术,我喜欢用static的风格自然会让我在使用Spring的时候各种水土不服。

这里只是用Java举了一个例子,文末我会附几个链接,都是说明为什么不应该在OOP里面使用静态方法的原因,有基于PHP的,有基于C#的,也有基于Java的,作者们的核心观点总结起来有两方面,一方面就是静态方法属于面向过程语言的思想,用在OOP里面会让很多OOP技术都不可用,比如上面的例子就属于这种,静态方法让程序丧失了可扩展性。另一方面就是不利于自动化的单元测试,因为单元测试基本都是使用Mock等技术创造一些假的依赖类的实例,也是基于实例方法的。如果实现为静态方法,那这种技术也无法使用了,所以静态方法也是单元测试的噩梦。更有激进的作者认为,像Math.abs(-5)这种标准库的实现是因为语言设计的不对(比如Java),正确设计应该是-5.abs(),比如Ruby就是这样设计的。

本文写的比较简单,结论就是:在OOP中,慎用静态方法。如果还不足以说(shuo,这个不读shui)服你的话,可以看一下下面的几篇文章,我觉得非常不错:

]]>
0 http://niyanchun.com/use-static-in-oop-carefully.html#comments http://niyanchun.com/feed/category/coding/