代码简洁之路——一些让你代码更加简洁和清晰的JAVA特性

以上内容都是基于JAVA 8 的版本。对于之后的JAVA新特性并没有整理进来。


平时我们开发功能时,总希望能用更简洁的代码实现业务需求,代码简洁一向都是各个开发人员追求的目标。

就像是为了减少get、set我们可以使用lombok插件,为了方便进行SQL查询而使用MyBatis-Plus增强。而随着java版本不断的更新,Java也在语言层面上提供更多便利给开发人员。

如何使自己的代码内容更加少,而可以实现的功能更多。最常提起的就是Java各个版本提供的语法糖。而关于语法糖的内容已经有足够多人去介绍了,这里我只是会介绍一些平时使用过的语法糖,而对于:基础类型的自动装箱泛型擦除条件编译枚举switch 增强。这些已经被广泛使用的内容就不再介绍了。

数值分割

在java 1.7中,我们在设置数值的时候,可以在数字之间插入任意多个下划线。这样对于一些比较大的数字,可以通过下划线来切割,方便我们阅读。这是一个小的改进,虽然并没有多少复杂的逻辑在里面,但是经常在配置文件中设置比较大的数值的需求,使用这个小技巧会让你的数值更加被区分出来。

    public long getLong() {
        long rest = 1_000_000_000L;
        return rest;
    }

try-with-resource

try-with-resource是JAVA为了方便对资源进行关闭而提供的语法糖。在此之前我们每次使用资源操作之后往往需要频繁的嵌套大量try-catch和null的判断来保证所有资源都被正常关闭。而使用try-with-resource代码会在编译阶段才会补全这部分内容

try-with-resource语法的使用

我们需要将操作的资源在 try 之后的括号中声明,然后就可以在后面的代码块中使用,就像是下面这样子

public class ResourceSugar {

    public void testResource() {
        try (FileReader fileReader = new FileReader("D:\\ web.xml");
             BufferedReader br = new BufferedReader(fileReader)) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println("do something...");
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

}

上面是一个使用try-with-resource的例子,而其实现的原理就是在编译的时候将我们关闭资源时候需要的循环try-catch内容进行补全。将上面的内容进行编译后可以得到下面的代码。

编译后

public void testResource() {
        try {
            FileReader fileReader = new FileReader("D:\\ web.xml");
            Throwable var2 = null;

            try {
                BufferedReader br = new BufferedReader(fileReader);
                Throwable var4 = null;

                try {
                    while(br.readLine() != null) {
                        System.out.println("do something...");
                    }
                } catch (Throwable var29) {
                    var4 = var29;
                    throw var29;
                } finally {
                    if (br != null) {
                        if (var4 != null) {
                            try {
                                br.close();
                            } catch (Throwable var28) {
                                var4.addSuppressed(var28);
                            }
                        } else {
                            br.close();
                        }
                    }

                }
            } catch (Throwable var31) {
                var2 = var31;
                throw var31;
            } finally {
                if (fileReader != null) {
                    if (var2 != null) {
                        try {
                            fileReader.close();
                        } catch (Throwable var27) {
                            var2.addSuppressed(var27);
                        }
                    } else {
                        fileReader.close();
                    }
                }

            }

        } catch (IOException var33) {
            throw new RuntimeException(var33);
        }
    }

可以看到try-with-resource编译后就能看到我们平时冗长当try-catch嵌套的流关闭操作,而使用try-with-resource语法后,这一步将不再需要我们自己驱编写。从而大大减少代码量。

try-with-resource使用时需要注意的问题

不允许修改声明的资源变量

为了防止在try内的代码块中修改变量引用从而导致编译的资源关闭逻辑失效,从而try-with-resource中声明的变量会隐式的加上final 关键字,比如下面代码将无法编译

    public void testResource() {
        try (FileReader fileReader = new FileReader("D:\\ web.xml");
             BufferedReader br = new BufferedReader(fileReader)) {
            fileReader = new FileReader("D:\\ web.xml")
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println("do something...");
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

错误的资源变量创建方式,导致无法正常关闭资源

在开发中有些人可能因为盲目相信try-with-resource可以自动关闭资源,这个时候可能会使用流嵌套的写法比如下面这样

    public void testResource() {
        try (BufferedReader br = new BufferedReader(new FileReader("D:\\ web.xml"))) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println("do something...");
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

看起来并没有太大问题,但是这个时候查看上面内容编译后的代码

    public void testResource() {
        try {
            BufferedReader br = new BufferedReader(new FileReader("D:\\ web.xml"));
            Throwable var2 = null;

            try {
                while(br.readLine() != null) {
                    System.out.println("do something...");
                }
            } catch (Throwable var12) {
                var2 = var12;
                throw var12;
            } finally {
                if (br != null) {
                    if (var2 != null) {
                        try {
                            br.close();
                        } catch (Throwable var11) {
                            var2.addSuppressed(var11);
                        }
                    } else {
                        br.close();
                    }
                }

            }

        } catch (IOException var14) {
            throw new RuntimeException(var14);
        }
    }

可以看到编译后的代码少了很多,此时因为BufferedReader声明了变量,try-with-resource只会尝试对BufferedReader进行关闭。而内部的FileReader进行资源关闭的代码却不见了。所以在使用try-with-resources的时候一定要为各个资源声明独立变量。

可变参数

在开发中,有些时候我们希望一个方法能够接受1-n个参数。在之前我们可能会把这样的参数声明为一个数组或者集合,但是我们也可以使用可变参数。

创建使用可变参数的方法

要想定义一个可变参数,只需要在参数类型后面使用…标识,就像下面操作

    public static void setObj(Object... params) {
        System.out.println(params.length);
    }

这样我们可以使用下面的方式调用上面参数

可变参数使用

在使用可变参数的方法时候,可变参数会被认为一个数组参数。所以使用可变参数的方法时可以多个参数使用,也可以将可变参数拼装成数组使用。下面两种参数都是可以正确使用

setIntegerToObj(1,2,3);
setIntegerToObj(new Integer[]{1, 2, 3});

可变参数限制

使用可变参数的时候并不会限制方法有多少其他类型的参数也不要求其他参数类型和可变参数是否相同,但是可变参数必须在最后一位。

可变参数使用需要注意的问题

使用可变参数的时候对于重载和重写有比较多的要求,不过好在现在的开发工具都会及时的抛出相关异常这里就不介绍了。

下面介绍一个自己曾经遇见的问题。下面的main方法中最终的结果大家觉得会是多少?

public class ParamSugar {

    public static void main(String[] args) {
        setIntToObj(1,2,3);
        setIntegerToObj(new Integer[]{1, 2, 3});
        setObj(1,2,3);
    }

    public static void setIntToObj(int... params) {
        setObj(params);
    }

    public static void setIntegerToObj(Integer... params) {
        setObj(params);
    }

    public static void setObj(Object... params) {
        System.out.println(params.length);
    }

}

这里我介绍下上面问题出现的场景,正常我们一般不会写一个数据类型object的可变参数,object的类型会使得方法的重载变得更容易出现问题,但是在一些常用的工具上(比如:MongodbTemplate的Criteria所使用的批量条件的方法)会经常遇见object的可变参数,而这个时候上面的场景就可能出现。

而上面内容最终会输出下面的结果:

1
3
3

可以看到,当直接使用int参数的时候,setObj识别为3个参数,而使用Integer的数组在被setIntegerToObj中转一次后也会被识别为3个参数,但是使用setIntToObj中转一次后却被setObj识别为了1个元素。

这里会发现虽然对于可变参数我们使用数组作为参数调用在方法内也可以正确使用,但是这只是针对一般对象或者基础类型的包装类。当我们直接使用setObj的时候三个数字会被自动装箱成三个包装类,而我将三个基础类型拼装为数组后,setObj只会将其识别为一个数组类型的参数。

增强的for循环

对于数据的循环,在开发中应该是使用最多的场景,最开始我们使用fori循环。的时候我们需要使用当前索引获取当前目标的元素。但是使用新的For循环可以省去获取当前元素的操作。

下面就是增强的for循环使用方法。

public class ForEachSugar {

    public void testForEach() {
        List<String> rest = new ArrayList<>();
        rest.add("one");
        rest.add("two");
        rest.add("three");
        for (String item : rest) {
            item = item + "test";
            System.out.println(item);
        }
    }
}

for括号中三个值分别为(集合元素类型 当前的元素 : 集合)。而此循环底层使用的还是迭代器,对上面代码编译后可以看到下面内容

public class ForEachSugar {
    public ForEachSugar() {
    }

    public void testForEach() {
        List<String> rest = new ArrayList();
        rest.add("one");
        rest.add("two");
        rest.add("three");
        Iterator var2 = rest.iterator();

        while(var2.hasNext()) {
            String item = (String)var2.next();
            item = item + "test";
            System.out.println(item);
        }
    }
}

从编译后的代码可以看出来foreach其实就是使用了迭代器,只不过是简化了迭代器的使用,变得更加简单。这样使得循环变得更加简洁。

使用for循环需要注意的内容

使用迭代器的话,这个时候就需要注意,在循环中尝试修改集合长度将会出现ConcurrentModificationException错误。但是对于元素为对象的集合可以通过foreach修改其对象内属性是没有问题的。

ps.关于JAVA的循环,对于增强的for循环目前看起来的确可以提高循环的简洁性,但是实际中我个人使用的不算多,因为随着下面的函数式接口Java提供了更加强大的对集合操作的支持。这里我稍晚会单独分出一部分来介绍Lambda和Stream结合起来的集合操作。

@FunctionalInterface(函数式接口)

为什么会有这个接口

Java是一种面向对象的语言,我们可以将对象作为参数进行传递,但是我们不能将对象的方法作为参数进行传递。而在Java Lambda的实现中,开发组不想再为Lambda表达式单独定义一种特殊的Structural函数类型(增加一个结构化的函数类型会增加函数类型的复杂性,破坏既有的Java类型,并对成千上万的Java类库造成严重的影响),因此最终还是利用SAM 接口作为 Lambda表达式的目标类型。只要接口中只定义了唯一的抽象方法的接口那它就是一个实质上的函数式接口,就可以用来实现Lambda表达式。

对于这个接口我曾经有过一个学习笔记 函数式接口学习。当然对于当时的我并没有意识到这个接口对Java后续的影响。

下面是对此接口的一个简单使用。

// 接口
public interface UserFunction {

    User handleUser(User value);
}

public class FunctionalSugar {

    // Functional1,Functional2,Functional3都实现了UserFunction接口
    public void testFunctiona (int type) {
        User user = new User();
        switch (type) {
            case 1:
                handle(new Functional1()::handleUser,user);
                break;
            case 2:
                handle(new Functional2()::handleUser,user);
                break;
            default:
                handle(new Functional3()::handleUser,user);
                break;
        }
    }

    public void handle(UserFunction userFunction, User user) {
        userFunction.handleUser(user);
    }
}

函数式接口的影响

通过函数式接口使得我们将某个对象的方法作为参数进行传递成为可能。再此之前希望根据方法不同实现不同的操作这种业务,我们只能是使用Method类配合反射操作进行方法的调用,这种显然有非常大的局限性,并且有风险的。

方法引用

配合上面提到的函数式接口,Java提供了直接引用已有Java类或对象的方法的功能。

**可以引用那些方法 **

类型语法Lambda表达式
静态方法类::staticMethod(args) -> 类.staticMethod(args)
实例方法引用实例::instMethod(args) -> 实例.instMethod(args)
构建方法类::new(args) -> new 类(args)

方法引用的使用

每个类的方法根据其参数和结果都会返回其对应的函数式接口对象。

public class MethodReferencesSugar {

    public void testMethodReferences() {
        User user = new User("user","desc");
        BiConsumer<User, String> setDesc = User::setDesc;
        Consumer<String> setDesc1 = user::setDesc;
        Consumer<String> setName = user::setName;
    }
}

使用这种方式我们可以将对象中的方法随意传递,更重要的是这种操作并不需要我们对之前的代码进行任何修改。也不需要自己创建任何新的接口,这这要归功于Java内置了大量@FunctionalInterface的接口。

在这里插入图片描述

方法引用带来的变化

使用方法引用可以根据条件为相同的参数提供不同的方法进行执行,比如下面内容我们可以将某个对象设置值的方法作为
参数传递到某些公共方法中,在方法中我们只关注使用函数式方法设置什么样的内容,而无需关注给哪个对象设置哪个参数。这样对于某些业务中会使我们的注意力用来关注在更加重要的内容上。

public class MethodReferencesSugar {

    public void testMethodReferences() {
        User user = new User("user","desc");
        BiConsumer<User, String> setDesc = User::setDesc;
        Consumer<String> setDesc1 = user::setDesc;
        setDesc1.accept("设置desc");
        Consumer<String> setName = user::setName;
        setName.accept("设置name");
    }
}

循环中的方法引用

Java本身对某些循环就提供了接收函数式参数的方法,而在这个时候我们可以直接调用一个方法的引用,而无需再设置参数的代码。

    public static void testForEach3() {
        List<User> rest = new ArrayList<>();
        rest.add(new User("one","one"));
        rest.add(new User("two","two"));
        rest.add(new User("three","three"));
        rest.forEach(System.out::println);
    }

Lambda表达式

Lambda表达式,本人的看法,绝对是这几年来Java最大的改进。配合Lambda表达式Java在尝试将代码变的简洁和优雅的道路前进的更加迅速。Lambda表达式让代码更加简洁,让我们把注意力更加关注在业务上而不是枯燥的调用以及循环上, 。配合Lambda表达式,Java在方法调用、集合循环上都提供了更加优秀的功能。在代码风格上,Lambda表达式成为新旧代码风格的分水岭。

Lambda表达式的语法

Java中Lambda 表达式由三个部分组成

  1. 第一部分为用逗号分隔的参数,外侧使用()包裹(有些时候可以省略)
  2. 中间为 ->
  3. 第三部分为方法体,对于单行代码可以直接输入其内容,对于多行代码可以使用{}包裹

最终使用方式类似下面的行为

(参数(如果有的话)) -> {代码块} 

什么时候使用Lambda表达式

Lambda表达式本质上是对函数式接口的的使用,所以当一个方法接收的参数为函数式接口的时候,调用此类方法的时候其函数式接口的位置就可以使用Lambda表达式

循环中Lambda的使用

Java中使用forEach和stream都接收一个函数式接口作为参数,所以在循环的时候都可以使用Lambda表达式。

    public static void testForEach3() {
        List<User> rest = new ArrayList<>();
        rest.add(new User("one","one"));
        rest.add(new User("two","two"));
        rest.add(new User("three","three"));
        rest.forEach(item -> {
            System.out.println(item.getDesc());
        });
    }

    public static void testForEach4() {
        HashMap<String,User> rest = new HashMap<>();
        rest.put("one",new User("one","one"));
        rest.put("two",new User("two","two"));
        rest.put("three",new User("three","three"));
        rest.forEach((k,v) -> {
            System.out.println(k);
            System.out.println(v.getDesc());
        });
    }

自定义函数接口的使用

只要接口形式符合函数式接口的定义都可以识别为函数式接口,所以当一个接口实现了其规则,也可以通过Lambda表达式来直接实现其业务。就像下面内容

// 函数式接口
public interface UserFunction {

    User handleUser(User value);
}
// 具体使用
public class FunctionalSugar {

    // Functional1,Functional2,Functional3都实现了UserFunction接口
    public void testFunctiona (int type) {
        User user = new User();
        // 声明一个函数式接口实现
        UserFunction userFunction = value -> {value.setDesc("");return value;};
        switch (type) {
            case 1:
                handle(userFunction,user);
                break;
            case 2:
                // 一个匿名的函数式接口
                handle(value -> {value.setDesc("");return value;},user);
                break;
            default:
                handle(new Functional3()::handleUser,user);
                break;
        }
    }
    // 支持函数式接口的参数
    public void handle(UserFunction userFunction,User user) {
        userFunction.handleUser(user);
    }
}

Lambda表达式和匿名内部类的区别

从上面例子可以看出来Lambda表达式很类似匿名内部类。就是对接口的直接实现。但是匿名内部类和Lambda实现的类在this指代的上下文是不一样的。Lambda中的this指向的是外部,对于上面的例子中,在Lambda表达式中的代码块中使用this可以调用FunctionalSugar的方法属性,而假如使用实现相关接口,其this则指向自身的引用。

不要过度使用Lambda表达式

我们使用Lambda表达式主要是为了让代码更加简洁,关注我们需要关注的内容。所以当需要实现的业务逻辑比较多的时候,我们需要将逻辑单独抽出来使用。比如下面业务中,在Lambda代码块中可能需要嵌套大量逻辑,此时就不需要强行在方法中写这么多内容。可以单独写一个方法使用,亦或者在有些场景下,多个Lambda表达式放在一起代码可能被简化了很多,但是其理解难度也会提高。

public class FunctionalSugar {

    public void testFunctiona () {
        User user = new User();
        handle(value -> {
            value.setDesc("");
            // 业务2
            // 业务3
            // 业务4
            // ......
            // 业务n
            return value;},user);
    }

    public void handle(UserFunction userFunction,User user) {
        userFunction.handleUser(user);
    }

}
public class FunctionalSugar {

    public void testFunctiona () {
        User user = new User();
        handle(value -> someThings(value),user);
    }

    public void handle(UserFunction userFunction,User user) {
        userFunction.handleUser(user);
    }

    public User someThings(User user) {
        user.setDesc("");
        // 业务2
        // 业务3
        // 业务4
        // ......
        // 业务n
        return user;
    }

}


个人水平有限,上面的内容可能存在没有描述清楚或者错误的地方,假如开发同学发现了,请及时告知,我会第一时间修改相关内容。假如我的这篇内容对你有任何帮助的话,麻烦给我点一个赞。你的点赞就是我前进的动力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大·风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值