Java業務開發常見錯誤100例筆記

一、多線程相關

1、ThreadLocal

ThreadLocal適用於變量在線程間隔離,而在方法或類間共享的場景

程序運行在Tomcat中,執行程序的線程是Tomcat的工作線程,而Tomcat的工作線程是基於線程池的。線程池會重用固定的幾個線程,所以使用ThreadLocal來存放一些數據時,需要特別注意在代碼運行完後,需要在代碼的finally代碼塊中,顯式清除ThreadLocal中的數據

2、ConcurrentHashMap

ConcurrentHashMap只能保證提供的原子性讀寫操作是線程安全的

  • 使用了ConcurrentHashMap,不代表對它的多個操作之間的狀態是一致的,如果需要確保需要手動加鎖
  • 諸如size()isEmpty()containsValue()等聚合方法,在併發情況下可能會反映ConcurrentHashMap的中間狀態。因此在併發情況下,這些方法的返回值只能用作參考,而不能用於流程控制
  • 諸如putAll()這樣的聚合方法也不能確保原子性,在putAll()的過程中去獲取數據可能會獲取到部分數據

3、CopyOnWriteArrayList

CopyOnWriteArrayList雖然是一個線程安全的ArrayList,但因爲其實現方式是,每次修改數據時都會複製一份數據出來,適用於讀多寫少或者說希望無鎖讀的場景。如果讀寫比例均衡或者有大量寫操作的話,使用CopyOnWriteArrayList的性能會非常糟糕

二、Spring事務

1、@Transactional生效策略

  • 除非特殊配置(比如使用AspectJ靜態織入實現AOP),否則只有定義在public方法上的@Transactional才能生效。原因是,Spring默認通過動態代理的方式實現AOP,對目標方法進行增強,private方法無法代理到,Spring自然也無法動態增強事務處理邏輯
  • 必須通過代理過的類從外部調用目標方法才能生效

2、事務回滾

  • 只有異常傳播出了標記了@Transactional註解的方法,事務才能回滾
  • 默認情況下,出現RuntimeExceptionError的時候,Spring纔會回滾事務

三、判等問題

  • 對基本類型,比如int、long,進行判等,只能使用==,比較的是直接值。因爲基本類型的值就是其數值
  • 對引用類型,比如Integer、Long和String,進行判等,需要使用equals()進行內容判等。因爲引用類型的直接值是指針,使用==的話,比較的是指針,也就是兩個對象在內存中的地址,即比較它們是不是同一個對象,而不是比較對象的內容

比較值的內容,除了基本類型只能使用==外,其他類型都需要使用equals()

1、Integer與int

        //案例一
        Integer a = 127; //Integer.valueOf(127)
        Integer b = 127; //Integer.valueOf(127)
        System.out.println("\nInteger a = 127;\n" + "Integer b = 127;\n" + "a == b ? " + (a == b)); //true

        //案例二
        Integer c = 128; //Integer.valueOf(128)
        Integer d = 128; //Integer.valueOf(128)
        System.out.println("\nInteger c = 128;\n" + "Integer d = 128;\n" + "c == d ? " + (c == d)); //false

        //案例三
        Integer e = 127; //Integer.valueOf(127)
        Integer f = new Integer(127); //new instance
        System.out.println("\nInteger e = 127;\n" + "Integer f = new Integer(127);\n" + "e == f ? " + (e == f)); //false

        //案例四
        Integer g = new Integer(127); //new instance
        Integer h = new Integer(127); //new instance
        System.out.println("\nInteger g = new Integer(127);\n" + "Integer h = new Integer(127);\n" + "g == h ? " + (g == h)); //false

        //案例五
        Integer i = 128; //unbox
        int j = 128;
        System.out.println("\nInteger i = 128;\n" + "int j = 128;\n" + "i == j ? " + (i == j)); //true

案例一,編譯器會把Integer a = 127轉換爲Integer.valueOf(127),轉換在內部其實做了緩存,使得兩個Integer指向同一個對象,所以==返回true,默認會緩存[-128, 127]的數值,所以案例二==返回false

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

案例三和案例四中,new出來的Integer始終是不走緩存的新對象。比較兩個新對象,或者比較一個新對象和一個來自緩存的對象,結果肯定不是相同的對象,因此返回false

案例五中,把裝箱的Integer和基本類型int比較,前者會先拆箱再比較,比較的肯定是數值而不是引用,因此返回true

2、String

        String a = "1";
        String b = "1";
        System.out.println("\nString a = \"1\";\n" + "String b = \"1\";\n" + "a == b ? " + (a == b)); //true

        String c = new String("2");
        String d = new String("2");
        System.out.println("\nString c = new String(\"2\");\n" + "String d = new String(\"2\");\n" + "c == d ? " + (c == d)); //false

        String e = new String("3").intern();
        String f = new String("3").intern();
        System.out.println("\nString e = new String(\"3\").intern();\n" + "String f = new String(\"3\").intern();\n" + "e == f ? " + (e == f)); //true

        String g = new String("4");
        String h = new String("4");
        System.out.println("\nString g = new String(\"4\");\n" + "String h = new String(\"4\");\n" + "g == h ? " + g.equals(h)); //true

Java的字符串常量池機制設計初衷是節省內存。當代碼中出現雙引號形式創建字符串對象時,JVM會先對這個字符串進行檢查,如果字符串常量池中存在相同內容的字符串對象的引用,則將這個引用返回;否則,創建新的字符串對象,然後將這個引用放入字符串常量池,並返回該引用。這種機制,就是字符串駐留池化

案例一返回 true,因爲Java的字符串駐留機制,直接使用雙引號聲明出來的兩個String對象指向常量池中的相同字符串

案例二,new出來的兩個String是不同對象,引用當然不同,所以得到false的結果

案例三,使用String提供的intern()方法也會走常量池機制,所以同樣能得到true

案例四,通過equals()對值內容判等,是正確的處理方式,當然會得到true

雖然使用new聲明的字符串調用intern()方法,也可以讓字符串進行駐留,但在業務代碼中濫用intern(),可能會產生性能問題

3、實現equals方法

對於自定義類型,如果不重寫equals()的話,默認就是使用Object基類的按引用的比較方式

String的equals()的實現

    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

重寫equals()的步驟

  • 考慮到性能,可以先進行指針判等,如果對象是同一個那麼直接返回true
  • 需要對另一方進行判空,空對象和自身進行比較,結果一定是fasle
  • 需要判斷兩個對象的類型,如果類型都不同,那麼直接返回false
  • 確保類型相同的情況下再進行類型強制轉換,然後逐一判斷所有字段

重寫equals方法時總要重寫hashCode

public class Point {
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Point that = (Point) o;
        return x == that.x && y == that.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

}

4、Lombok使用

Lombok的@Data註解會幫我們實現equals()hashcode()方法

@Data
public class Person {
    private String name; //姓名
    private String identity; //身份證

    public Person(String name, String identity) {
        this.name = name;
        this.identity = identity;
    }
}

對於身份證相同、姓名相同的兩個Person對象

        Person person1 = new Person("xiaoming", "001");
        Person person2 = new Person("xiaoming", "001");
        System.out.println("person1.equals(person2) ? " + person1.equals(person2)); //true

如果只要身份證一致就認爲是同一個人的話,可以使用@EqualsAndHashCode.Exclude註解來修飾name字段,從equals()hashCode()的實現中排除name字段:

@Data
public class Person {
    @EqualsAndHashCode.Exclude
    private String name; //姓名

    private String identity; //身份證

    public Person(String name, String identity) {
        this.name = name;
        this.identity = identity;
    }
}
        Person person1 = new Person("xiaoming", "001");
        Person person2 = new Person("xiaohong", "001");
        System.out.println("person1.equals(person2) ? " + person1.equals(person2)); //true

Employee類繼承Person,並新定義一個公司屬性

@Data
public class Employee extends Person {
    private String company;

    public Employee(String name, String identity, String company) {
        super(name, identity);
        this.company = company;
    }
}

聲明兩個Employee實例,它們具有相同的公司名稱,但姓名和身份證均不同,結果返回爲true

        Employee employee1 = new Employee("zhuye", "001", "bkjk.com");
        Employee employee2 = new Employee("Joseph", "002", "bkjk.com");
        System.out.println("employee1.equals(employee2) ? " + employee1.equals(employee2)); //true

@EqualsAndHashCode默認實現沒有使用父類屬性,可以手動設置callSuper開關爲true

@Data
@EqualsAndHashCode(callSuper = true)
public class Employee extends Person {

四、數值計算

1、BigDecimal使用

小數點的加減乘除都使用BigDecimal來解決,因爲double或者float會丟失精度

  • 使用BigDecimal表示和計算浮點數,且務必使用字符串的構造方法來初始化BigDecimal
  • 如果一定要用Double來初始化BigDecimal的話,可以使用BigDecimal.valueOf()方法

2、丟失精度原因

        double a = 0.3;
        double b = 0.1;
        System.out.println(a - b); //0.19999999999999998
        BigDecimal bigDecimal = new BigDecimal(0.3);
        System.out.println(bigDecimal); //0.299999999999999988897769753748434595763683319091796875

對於十進制的小數轉換成二進制採用乘2取整法進行計算,取掉整數部分後,剩下的小數繼續乘以2,直到小數部分全爲0

將0.3轉成二進制的過程:
0.3 * 2 = 0.6 => .0 (.6)取0剩0.6
0.6 * 2 = 1.2 => .01 (.2)取1剩0.2
0.2 * 2 = 0.4 => .010 (.4)取0剩0.4
0.4 * 2 = 0.8 => .0100 (.8) 取0剩0.8
0.8 * 2 = 1.6 => .01001 (.6)取1剩0.6
.............

由於double不能精確表示爲0.3,因此用double構造函數傳遞的值不完全等於0.3。使用BigDecimal時,必須使用String字符串參數構造方法來創建它。BigDecimal是不可變的,在進行每一步運算時,都會產生一個新的對象。double的問題是從小數點轉換到二進制丟失精度,二進制丟失精度。而BigDecimal在處理的時候把十進制小數擴大N倍讓它在整數上進行計算,並保留相應的精度信息

3、equals做判等

        System.out.println(new BigDecimal("1.0").equals(new BigDecimal("1"))); //false

BigDecimal的equals()方法比較的是BigDecimal的value和scale,1.0的scale是1,1的scale是0,所以結果是false

如果希望只比較BigDecimal的value,可以使用compareTo()方法

        System.out.println(new BigDecimal("1.0").compareTo(new BigDecimal("1")) == 0); //true

BigDecimal的equals()hashCode()方法會同時考慮value和scale,如果結合HashSet或HashMap使用的話就可能會出現麻煩。比如,把值爲1.0的BigDecimal加入HashSet,然後判斷其是否存在值爲1的BigDecimal,得到的結果是false:

        Set<BigDecimal> hashSet1 = new HashSet<>();
        hashSet1.add(new BigDecimal("1.0"));
        System.out.println(hashSet1.contains(new BigDecimal("1"))); //false

解決這個問題的辦法有兩個

1)使用TreeSet替換HashSet。TreeSet不使用hashCode()方法,也不使用equals()比較元素,而是使用compareTo()方法,所以不會有問題

        Set<BigDecimal> treeSet = new TreeSet<>();
        treeSet.add(new BigDecimal("1.0"));
        System.out.println(treeSet.contains(new BigDecimal("1"))); //true

2)把BigDecimal存入HashSet或HashMap前,先使用stripTrailingZeros()方法去掉尾部的零,比較的時候也去掉尾部的0,確保value相同的BigDecimal,scale也是一致的

        Set<BigDecimal> hashSet2 = new HashSet<>();
        hashSet2.add(new BigDecimal("1.0").stripTrailingZeros());
        System.out.println(hashSet2.contains(new BigDecimal("1.000").stripTrailingZeros())); //true

五、Arrays.asList把數據轉換爲List

1、不能直接使用Arrays.asList來轉換基本類型數組

        int[] arr = {1, 2, 3};
        List<int[]> list = Arrays.asList(arr);
        System.out.println(list.size()); //1

只能是把int裝箱爲Integer,不可能把int數組裝箱爲Integer數組。Arrays.asList()方法傳入的是一個泛型T類型可變參數,最終int數組整體作爲了一個對象成爲了泛型類型T

2、Arrays.asList返回的List不支持增刪操作

Arrays.asList()返回的List並不是java.util.ArrayList,而是Arrays的內部類ArrayList。ArrayList內部類繼承自AbstractList類,並沒有覆寫父類的add()方法,而父類中add()方法的實現,就是拋出UnsupportedOperationException

    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

    private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable
    {
        private static final long serialVersionUID = -2764017481108945198L;
        private final E[] a;

        ArrayList(E[] array) {
            a = Objects.requireNonNull(array);
        }

        @Override
        public E get(int index) {
            return a[index];
        }

        @Override
        public E set(int index, E element) {
            E oldValue = a[index];
            a[index] = element;
            return oldValue;
        }

        //...
    }

3、對原始數組的修改會影響到通過Arrays.asList獲得的那個List

ArrayList的實現是直接使用了原始的數組。所以,把通過Arrays.asList()獲得的List交給其他方法處理,很容易因爲共享了數組,相互修改產生Bug

修復方式比較簡單,重新new一個ArrayList初始化Arrays.asList()返回的List即可

        String[] arr = {"1", "2", "3"};
        List list = new ArrayList(Arrays.asList(arr));
        arr[1] = "4";
        list.add("5");

六、Map是否支持空值

key爲null value爲null
HashMap 支持 支持
ConcurrentHashMap 不支持 不支持
Hashtable 不支持 不支持
TreeMap 不支持 支持

1、ConcurrentHashMap和Hashtable不允許空值的原因

主要是因爲會產生歧義,如果支持空值,在使用map.get(key)時,返回值爲null,可能有兩種情況:該key映射的值爲null,或者該key未映射到。如果是非併發映射中,可以使用map.contains(key)進行檢查,但是在併發的情況下,兩次調用之間的映射可能已經更改了

2、TreeMap對空值的支持

TreeMap線程不安全,但是因爲需要排序,進行key的compareTo()方法,所以key是不能null值,value是可以的

七、日期類

1、初始化日期時間

Date的構造函數中,年應該是和1900的差值,月應該是從0到11而不是從1到12

        Date date = new Date(2020 - 1900, 11, 31, 10, 28, 30);
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //2020-12-31 10:28:30
        System.out.println(formatter.format(date));

Calendar的構造函數中,初始化時年參數直接使用當前年即可,月還是從0到11而不是從1到12

        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Calendar calendar = Calendar.getInstance();
        calendar.set(2020, 11, 31, 10, 28, 30);
        //2020-12-31 10:28:30(當前時區)
        System.out.println(formatter.format(calendar.getTime()));
        Calendar calendar2 = Calendar.getInstance(TimeZone.getTimeZone("America/New_York"));
        calendar2.set(2020, Calendar.DECEMBER, 31, 10, 28, 30);
        //2020-12-31 23:28:30(紐約時區)
        System.out.println(formatter.format(calendar2.getTime()));

2、時區問題

Date沒有時區的概念,保存的是一個時間戳,代表的是從1970年1月1日0點(Epoch時間)到現在的毫秒數

        System.out.println(new Date(0));
        System.out.println(TimeZone.getDefault().getID());

得到的是1970年1月1日8點。因爲我電腦當前的時區是中國上海,相比UTC時差+8小時:

Thu Jan 01 08:00:00 CST 1970
Asia/Shanghai

1)、字符串轉Date

對於同一個時間表示,比如2020-01-02 22:00:00,不同時區的人轉換成Date會得到不同的時間(時間戳)

        String dateStr = "2020-01-02 22:00:00";
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //默認時區解析時間表示
        Date date1 = formatter.parse(dateStr);
        System.out.println(date1);
        //紐約時區解析時間表示
        formatter.setTimeZone(TimeZone.getTimeZone("America/New_York"));
        Date date2 = formatter.parse(dateStr);
        System.out.println(date2);

把2020-01-02 22:00:00這樣的時間表示,對於當前的上海時區和紐約時區,轉化爲UTC時間戳是不同的時間:

Thu Jan 02 22:00:00 CST 2020
Fri Jan 03 11:00:00 CST 2020

對於同一個本地時間的表示,不同時區的人解析得到的UTC時間一定是不同的,反過來不同的本地時間可能對應同一個UTC

2)、Date轉字符串

同一個Date,在不同的時區下格式化得到不同的時間表示。比如,在我的當前時區和紐約時區格式化2020-01-02 22:00:00

        String stringDate = "2020-01-02 22:00:00";
        SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //同一Date
        Date date = inputFormat.parse(stringDate);
        //默認時區格式化輸出
        System.out.println(new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss Z]").format(date));
        //紐約時區格式化輸出
        TimeZone.setDefault(TimeZone.getTimeZone("America/New_York"));
        System.out.println(new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss Z]").format(date));

我當前時區的Offset(時差)是+8小時,對於-5小時的紐約,晚上10點對應早上9點:

[2020-01-02 22:00:00 +0800]
[2020-01-02 09:00:00 -0500]

3)、小結

要正確處理時區,在於存進去讀出來兩方面:存的時候,需要使用正確的當前時區來保存,這樣 UTC 時間纔會正確;讀的時候,也只有正確設置本地時區,才能把 UTC 時間轉換爲正確的當地時間

在這裏插入圖片描述

八、反射、註解和泛型

1、反射調用方法不是以傳參決定重載

反射的功能包括,在運行時動態獲取類和類成員定義,以及動態讀取屬性、調用方法

有兩個叫age的方法,入參分別是基本類型int和包裝類型Integer

public class ReflectionIssueApplication {
    public void age(int age) {
        System.out.println("int age = " + age);
    }

    public void age(Integer age) {
        System.out.println("Integer age = " + age);
    }
}

使用反射時的誤區是,認爲反射調用方法還是根據入參確定方法重載

        Class<ReflectionIssueApplication> clazz = ReflectionIssueApplication.class;
        clazz.getDeclaredMethod("age", Integer.TYPE)
                .invoke(clazz.newInstance(), Integer.valueOf("36"));

執行結果:

int age = 36

要通過反射進行方法調用,第一步就是通過方法簽名來確定方法。具體到這個案例,getDeclaredMethod()傳入的參數類型Integer.TYPE代表的是int,所以實際執行方法時無論傳的是包裝類型還是基本類型,都會調用int入參的age方法

Integer.TYPE改爲Integer.class,執行的參數類型就是包裝類型的Integer。這時,無論傳入的是Integer.valueOf(“36”)還是基本類型的36

反射調用方法,是以反射獲取方法時傳入的方法名稱和參數類型來確定調用方法的

2、泛型經過類型擦除多出橋接方法的坑

父類是這樣的:有一個泛型佔位符T;有一個AtomicInteger計數器,用來記錄value字段更新的次數,其中value字段是泛型T類型的,setValue()方法每次爲value賦值時對計數器進行+1操作

public class Parent<T> {
    //用於記錄value更新的次數,模擬日誌記錄的邏輯
    AtomicInteger updateCount = new AtomicInteger();
    private T value;

    //重寫toString,輸出值和值更新次數
    @Override
    public String toString() {
        return String.format("value: %s updateCount: %d", value, updateCount.get());
    }

    //設置值
    public void setValue(T value) {
        System.out.println("Parent.setValue called");
        this.value = value;
        updateCount.incrementAndGet();
    }
}

子類Child1的實現是這樣的:繼承父類,但沒有提供父類泛型參數;定義了一個參數爲String的setValue()方法,通過super.setValue調用父類方法實現日誌記錄。開發人員這麼設計是希望覆蓋父類的setValue()實現

public class Child1 extends Parent {
    public void setValue(String value) {
        System.out.println("Child1.setValue called");
        super.setValue(value);
    }
}

子類方法的調用是通過反射進行的。實例化Child1類型後,通過getClass().getMethods()方法獲得所有的方法;然後按照方法名過濾出setValue()方法進行調用,傳入字符串test作爲參數

        Child1 child1 = new Child1();
        Arrays.stream(child1.getClass().getMethods())
                .filter(method -> method.getName().equals("setValue"))
                .forEach(method -> {
                    try {
                        method.invoke(child1, "test");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
        System.out.println(child1.toString());

執行結果:

Child1.setValue called
Parent.setValue called
Parent.setValue called
value: test updateCount: 2

父類的setValue()方法被調用了兩次,是因爲getClass().getMethods()方法找到了兩個名爲setValue的方法,分別是父類和子類的setValue()方法

這個案例中,子類方法重寫父類方法失敗的原因,包括兩方面:

  • 子類沒有指定String泛型參數,父類的泛型方法setValue(T value)在泛型擦除後是setValue(Object value),子類中入參是String的setValue()方法被當作了新方法
  • 子類的setValue()方法沒有增加@Override註解,因此編譯器沒能檢測到重寫失敗的問題。這就說明,重寫子類方法時,標記@Override是一個好習慣
public class Child2 extends Parent<String> {
    @Override
    public void setValue(String value) {
        System.out.println("Child2.setValue called");
        super.setValue(value);
    }
}

修復後,還是出現了重複記錄的問題:

Child2.setValue called
Parent.setValue called
Child2.setValue called
Parent.setValue called
value: test updateCount: 2

通過調試發現,Child2類其實有2個setValue()方法,入參分別是String和Object

在這裏插入圖片描述

Java 的泛型類型在編譯後擦除爲 Object。雖然子類指定了父類泛型 T 類型是 String,但編譯後 T 會被擦除成爲 Object,所以父類 setValue 方法的入參是 Object,value 也是 Object。如果子類 Child2 的 setValue 方法要覆蓋父類的 setValue 方法,那入參也必須是 Object。所以,編譯器會爲我們生成一個所謂的 bridge 橋接方法,實際上是入參爲 Object 的 setValue 方法在內部調用了入參爲 String 的 setValue 方法,也就是代碼裏實現的那個方法

使用jclasslib工具打開Child2類,同樣可以看到入參爲Object的橋接方法上標記了public + synthetic + bridge三個屬性。synthetic代表由編譯器生成的不可見代碼,bridge代表這是泛型類型擦除後生成的橋接代碼

在這裏插入圖片描述

通過getDeclaredMethods()方法獲取到所有方法後,必須同時根據方法名setValue和非isBridge兩個條件過濾,才能實現唯一過濾

        Child2 child2 = new Child2();
        Arrays.stream(child2.getClass().getMethods())
                .filter(method -> method.getName().equals("setValue") && !method.isBridge())
                .forEach(method -> {
                    try {
                        method.invoke(child2, "test");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
        System.out.println(child2.toString());

3、註解可以繼承嗎?

自定義的註解標註了@Inherited,子類可以自動繼承父類的該註解

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章