消灭 Java 代码的“坏味道”

399 篇文章 12 订阅
143 篇文章 1 订阅

简介: 阿里妹导读:代码中的"坏味道",如"私欲"如"灰尘",每天都在增加,一日不去清除,便会越累越多。如果用功去清除这些"坏味道",不仅能提高自己的编码水平,也能使代码变得"精白无一毫不彻"。这里,一直从事Java研发相关工作的阿里高级地图技术工程师王超,整理了日常工作中的一些"坏味道",及清理方法,供大家参考。

让代码性能更高

需要 Map 的主键和取值时,应该迭代 entrySet()

当循环中只需要 Map 的主键时,迭代 keySet() 是正确的。但是,当需要主键和取值时,迭代 entrySet() 才是更高效的做法,比先迭代 keySet() 后再去 get 取值性能更佳。

反例:

Map<String, String> map = ...;
for (String key : map.keySet()) {
    String value = map.get(key);
    ...
}

正例:

Map<String, String> map = ...;
for (Map.Entry<String, String> entry : map.entrySet()) {
    String key = entry.getKey();
    String value = entry.getValue();
    ...
}

集合初始化尽量指定大小

java 的集合类用起来十分方便,但是看源码可知,集合也是有大小限制的。每次扩容的时间复杂度很有可能是 O(n) ,所以尽量指定可预知的集合大小,能减少集合的扩容次数。

反例:

int[] arr = new int[]{1, 2, 3};
List<Integer> list = new ArrayList<>();
for (int i : arr) {
    list.add(i);
}

正例:

int[] arr = new int[]{1, 2, 3};
List<Integer> list = new ArrayList<>(arr.length);
for (int i : arr) {
    list.add(i);
}

List 的随机访问

大家都知道数组和链表的区别:数组的随机访问效率更高。当调用方法获取到 List 后,如果想随机访问其中的数据,并不知道该数组内部实现是链表还是数组,怎么办呢?可以判断它是否实现 RandomAccess 接口。

正例:

// 调用别人的服务获取到list
List<Integer> list = otherService.getList();
if (list instanceof RandomAccess) {
    // 内部数组实现,可以随机访问
    System.out.println(list.get(list.size() - 1));
} else {
    // 内部可能是链表实现,随机访问效率低
}

频繁调用 Collection.contains 方法请使用 Set

在 java 集合类库中,List 的 contains 方法普遍时间复杂度是 O(n) ,如果在代码中需要频繁调用 contains 方法查找数据,可以先将 list 转换成 HashSet 实现,将 O(n) 的时间复杂度降为 O(1) 。

反例:

public boolean containsDuplicate(int[] nums) {
    ArrayList<Integer> list = otherService.getList();
    for (int i = 0; i < nums.length; i++) {
        if(list.contains(nums[i])) {// 时间复杂度O(n)
            return true;
        } else {
            list.add(nums[i]);
        }
    }
    return false;
}

正例:

public static boolean containsDuplicate(int[] nums) {
    Set<Integer> set = new HashSet<>();
    for (int i = 0; i < nums.length; i++) {
        if(set.contains(nums[i])) {// 时间复杂度O(1)
            return true;
        } else {
            set.add(nums[i]);
        }
    }
    return false;
}

 备注:

ArrayList与HashSet元素查找的时间复杂度区别,实际上就是底层的实现区别。

ArrayList本质就是通过数组实现的,查找一个元素是否包含要用到遍历,时间复杂度是O(n) 。
HashSetHashSet的查找是通过HashMap的KeySet来实现的,判断是否包含某个元素的实现,时间复杂度是O(1)。

ArrayList判断是否包含某个元素的源码:

public boolean contains(Object o) {
    return indexOf(o) >= 0;
}

public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++) {
            if (elementData[i] == null) {
                return i;
            }
        }
    } else {
        for (int i = 0; i < size; i++) {//从头遍历
            if (o.equals(elementData[i])) {
                return i;
            }
        }
    }
    return -1;
}

HashSet判断是否包含某个元素的源码实现:

public boolean contains(Object o) {
    return map.containsKey(o);
}

public boolean containsKey(Object key) {
    return getNode(hash(key), key) != null;
}

final Node<K, V> getNode(int hash, Object key) {
    Node<K, V>[] tab;
    Node<K, V> first, e;
    int n;
    K k;
    
    //直接通过hash确定元素位置,不用从头遍历
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            
        if (first.hash == hash && //always check first node
                ((k = first.key) == key || (key != null && key.equals(k)))) {
            return first;
        }
        if ((e = first.next) != null) {
            if (first instanceof TreeNode) {
                return ((TreeNode<K, V>)first).getTreeNode(hash, key);
            }
            do {
            	//部分情况下可能会继续遍历链表定位
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k)))) {
                    return e;
                }
            } while ((e = e.next) != null);
        }
    }
    return null;
}

让代码更优雅

长整型常量后添加大写 L

在使用长整型常量值时,后面需要添加 L ,必须是大写的 L ,不能是小写的 l ,小写 l 容易跟数字 1 混淆而造成误解。

反例:

long value = 1l;
long max = Math.max(1L, 5);

正例:

long value = 1L;
long max = Math.max(1L, 5L);

不要使用集合实现来赋值静态成员变量

对于集合类型的静态成员变量,不要使用集合实现来赋值,应该使用静态代码块赋值。

反例:

private static Map<String, Integer> map = new HashMap<String, Integer>() {
    {
        put("a", 1);
        put("b", 2);
    }
};

private static List<String> list = new ArrayList<String>() {
    {
        add("a");
        add("b");
    }
};

正例:

private static Map<String, Integer> map = new HashMap<>();
static {
    map.put("a", 1);
    map.put("b", 2);
};


private static List<String> list = new ArrayList<>();
static {
    list.add("a");
    list.add("b");
};

工具类应该屏蔽构造函数

工具类是一堆静态字段和函数的集合,不应该被实例化。但是,Java 为每个没有明确定义构造函数的类添加了一个隐式公有构造函数。所以,为了避免 java "小白"使用有误,应该显式定义私有构造函数来屏蔽这个隐式公有构造函数。

反例:

public class MathUtils {
    public static final double PI = 3.1415926D;
    public static int sum(int a, int b) {
        return a + b;
    }
}

正例:

public class MathUtils {
    public static final double PI = 3.1415926D;
    private MathUtils() {}
    public static int sum(int a, int b) {
        return a + b;
    }
}

使用String.valueOf(value)代替""+value

当要把其它对象或类型转化为字符串时,使用 String.valueOf(value) 比""+value 的效率更高。

反例:

int i = 1;
String s = "" + i;

正例:

int i = 1;
String s = String.valueOf(i);

过时代码添加 @Deprecated 注解

当一段代码过时,但为了兼容又无法直接删除,不希望以后有人再使用它时,可以添加 @Deprecated 注解进行标记。在文档注释中添加 @deprecated 来进行解释,并提供可替代方案。

正例:

/**
 * 保存
 *
 * @deprecated 此方法效率较低,请使用{@link newSave()}方法替换它
 */
@Deprecated
public void save(){
    // do something
}

优先使用常量或确定值来调用 equals 方法

对象的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals 方法。当然,使用 java.util.Objects.equals() 方法是最佳实践。

反例:

public void isFinished(OrderStatus status) {
    return status.equals(OrderStatus.FINISHED); // 可能抛空指针异常
}

正例:

public void isFinished(OrderStatus status) {
    return OrderStatus.FINISHED.equals(status);
}

public void isFinished(OrderStatus status) {
    return Objects.equals(status, OrderStatus.FINISHED);
}

备注:

Objects.equals() 和 String的.equals()方法的区别,来看下Objects.equals()的源码(比较的是数值):

public static boolean equals(Object a, Object b) {
    return (a == b) || (a != null && a.equals(b));
}

//Object的equals()方法
public boolean equals(Object obj) {
    return (this == obj);
}

我们接着看一下Objects.deepEquals()方法的源码(比较的是内容):

public static boolean deepEquals(Object a, Object b) {
    if (a == b) {
        return true;
    } else if (a == null || b == null) {
        return false;
    } else {
        return Arrays.deepEquals0(a, b);//调用下面的deepEquals0方法
    }
}

static boolean deepEquals0(Object e1, Object e2) {
    assert e1 != null;
    boolean eq;
    if (e1 instanceof Object[] && e2 instanceof Object[]) {
        eq = deepEquals ((Object[]) e1, (Object[]) e2);//调用下面的deepEquals方法
    } else if (e1 instanceof byte[] && e2 instanceof byte[]) {
        eq = equals((byte[]) e1, (byte[]) e2);//调用数组的equals方法
    } else if (e1 instanceof short[] && e2 instanceof short[]) {
        eq = equals((short[]) e1, (short[]) e2);
    } else if (e1 instanceof int[] && e2 instanceof int[]) {
        eq = equals((int[]) e1, (int[]) e2);
    } else if (e1 instanceof long[] && e2 instanceof long[]) {
        eq = equals((long[]) e1, (long[]) e2);
    } else if (e1 instanceof char[] && e2 instanceof char[]) {
        eq = equals((char[]) e1, (char[]) e2);
    } else if (e1 instanceof float[] && e2 instanceof float[]) {
        eq = equals((float[]) e1, (float[]) e2);
    } else if (e1 instanceof double[] && e2 instanceof double[]) {
        eq = equals((double[]) e1, (double[]) e2);
    } else if (e1 instanceof boolean[] && e2 instanceof boolean[]) {
        eq = equals((boolean[]) e1, (boolean[]) e2);
    } else {
        eq = e1.equals(e2);
    }
    return eq;
}

public static boolean deepEquals(Object[] a1, Object[] a2) {
    if (a1 == a2) {
        return true;
    }
    if (a1 == null || a2 == null) {
        return false;
    }
    int length = a1.length;
    if (a2.length != length) {
        return false;
    }

    for (int i = 0; i < length; i++) {
        Object e1 = a1[i];
        Object e2 = a2[i];

        if (e1 == e2) {
            continue;
        }
        if (e1 == null) {
            return false;
        }

        // Figure out whether the two elements are equal
        boolean eq = deepEquals0(e1, e2);

        if (!eq) {
            return false;
        }
    }
    return true;
}

枚举的属性字段必须是私有不可变

枚举通常被当做常量使用,如果枚举中存在公共属性字段或设置字段方法,那么这些枚举常量的属性很容易被修改。理想情况下,枚举中的属性字段是私有的,并在私有构造函数中赋值,没有对应的 Setter 方法,最好加上 final 修饰符。

反例:

public enum UserStatus {
    DISABLED(0, "禁用"),
    ENABLED(1, "启用");


    public int value;
    private String description;


    private UserStatus(int value, String description) {
        this.value = value;
        this.description = description;
    }


    public String getDescription() {
        return description;
    }


    public void setDescription(String description) {
        this.description = description;
    }
}

正例:

public enum UserStatus {
    DISABLED(0, "禁用"),
    ENABLED(1, "启用");


    private final int value;
    private final String description;


    UserStatus(int value, String description) {
        this.value = value;
        this.description = description;
    }


    public int getValue() {
        return value;
    }


    public String getDescription() {
        return description;
    }
}

小心String.split(String regex)

字符串 String 的 split 方法,传入的分隔字符串是正则表达式!部分关键字(比如.[]()| 等)需要转义。

反例:

"a.ab.abc".split("."); // 结果为[]
"a|ab|abc".split("|"); // 结果为["a", "|", "a", "b", "|", "a", "b", "c"]

正例:

"a.ab.abc".split("\\."); // 结果为["a", "ab", "abc"]
"a|ab|abc".split("\\|"); // 结果为["a", "ab", "abc"]

总结

这篇文章,可以说是从事 Java 开发的经验总结,分享出来以供大家参考。希望能帮大家避免踩坑,让代码更加高效优雅。

原文发布时间为:2019-10-11
作者:王超

以下是一个简单的 Java 实现: ```java import java.util.*; public class DestroyTheStars { public static void main(String[] args) { // 创建一个 10x10 的星星矩阵 boolean[][] stars = new boolean[10][10]; // 初始化星星矩阵,随机生成一些星星 Random rand = new Random(); for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { stars[i][j] = rand.nextBoolean(); } } // 输出初始的星星矩阵 System.out.println("初始的星星矩阵:"); for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { System.out.print(stars[i][j] ? "*" : "."); } System.out.println(); } // 输入要消灭的星星坐标 Scanner scanner = new Scanner(System.in); System.out.print("请输入要消灭的星星坐标(格式:行 列):"); int row = scanner.nextInt(); int col = scanner.nextInt(); // 检查输入坐标是否合法 if (row < 0 || row >= 10 || col < 0 || col >= 10) { System.out.println("输入坐标有误!"); return; } // 消灭星星 destroy(stars, row, col); // 输出消灭后的星星矩阵 System.out.println("消灭后的星星矩阵:"); for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { System.out.print(stars[i][j] ? "*" : "."); } System.out.println(); } } // 消灭星星的方法 private static void destroy(boolean[][] stars, int row, int col) { if (!stars[row][col]) { return; } stars[row][col] = false; if (row > 0) { destroy(stars, row - 1, col); } if (row < 9) { destroy(stars, row + 1, col); } if (col > 0) { destroy(stars, row, col - 1); } if (col < 9) { destroy(stars, row, col + 1); } } } ``` 运行程序,输入要消灭的星星坐标(行和列,从 0 开始),程序会输出消灭后的星星矩阵。程序使用递归算法实现星星消灭,从输入的坐标开始,将该位置的星星消灭,并递归消灭其周围的星星,直到所有相连的星星都被消灭
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值