JAVA碎碎念【一】


title: JAVA碎碎念【一】
date: 2018-03-04 20:10:21
tags: Java
categories: Java


過去的 2017 年,關(guān)于 Java 大大小小的坑還是踩了不少。這里回顧一下還記得的問題。

數(shù)組與List的轉(zhuǎn)換

常見做法:

List<String> list = Arrays.asList(arr);

問題:

多數(shù)我們都認(rèn)為Arrays.asList(arr)方法返回的是ArrayList.

實(shí)際上返回的是java.util.Arrays.ArrayList,其完整ArrayList聲明:

private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, Serializable

實(shí)現(xiàn)方法如下圖:

java.util.Arrays.ArrayList結(jié)構(gòu).jpg

java.util.Arrays.ArrayList并沒有實(shí)現(xiàn)所有ArrayList的方法

所以當(dāng)我們期望得到一個(gè)真正的ArrayList時(shí)需要如下操作:

ArrayList<String> arrayList = new ArrayList<String>(Arrays.asList(arr));

類似的問題:
java.util.ArrayList.subList() 返回的不是 ArrayList。當(dāng)在有些場(chǎng)景下需要依賴容器特性(例如是否序列化)時(shí)要確認(rèn)一下集合操作的返回類型,如有必要最好顯示的轉(zhuǎn)換。


集合中移除元素

例如,循環(huán)刪除List中所有元素,很容易寫出下面的代碼:

List<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d"));
for (int i = 0; i < list.size(); i++) {
  list.remove(i);
}

問題:
每次根據(jù)索引刪除元素后List的大小及元素索引會(huì)發(fā)生變化,需要使用迭代器實(shí)現(xiàn),使用迭代器很容易寫出下面的代碼:

List<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d"));
for (String s : list) {
  if (s.equals("a"))
    list.remove(s);
}

實(shí)際還是錯(cuò)誤的,正確寫法如下(我?guī)缀跷ㄒ挥玫鞯膱?chǎng)景):

List<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d"));
Iterator<String> iter = list.iterator();
while (iter.hasNext()) {
  String s = iter.next();
 
  if (s.equals("a")) {
    iter.remove();
  }
}

Integer.parseInt(String s)/valueOf(String s)

Integer.parseInt(String s)Integer.valueOf(String s) 的區(qū)別

將一個(gè)數(shù)字字符串轉(zhuǎn)數(shù)字,例如:

// “123” -> 123
String numStr = "123";
Integer.parseInt(numStr);
Integer.valueOf(numStr);

Integer.parseInt()Integer.valueOf() 的區(qū)別主要在于放回的類型上:

Integer.parseInt() 返回 int

Integer.valueOf()返回Integer

對(duì)性能敏感的地方或循環(huán)內(nèi)部,應(yīng)當(dāng)避免不必要的拆箱裝箱


ConcurrentMap.putIfAbsent(key,value)

錯(cuò)誤用法:

ConcurrentMap<Dept, Person> cache = new ConcurrentMap.putIfAbsent<>();
// ...
public Dept getDept(Person person) {
  Dept dept = cache.get(person);
  if (dept == null) {
    dept = person.getDept();
    cache.putIfAbsent(person, dept);
  }
  return dept;
}

putIfAbsent方法完整簽名如下:public V putIfAbsent(K var1, V var2)

看到返回值了有木有,這點(diǎn)很關(guān)鍵。

多線程環(huán)境下,可能線程A首先獲取失敗,當(dāng)A創(chuàng)建完value對(duì)象,準(zhǔn)備插入時(shí)已經(jīng)有別的線程插入了,這時(shí)候再返回A創(chuàng)建的value可能存在問題。

所以,如果當(dāng)前put失敗,則返回已存在的value,否則返回null。

正確的寫法:

ConcurrentMap<Dept, Person> cache = new ConcurrentMap.putIfAbsent<>();
// ...
public Dept getDept(Person person) {
  Dept dept = cachecache.get(person);
  if (dept == null) {
    dept = person.getDept();
    Dept deptPuted = cache.putIfAbsent(person, dept);
    if (deptPuted != null) {
      dept = deptPutted;
    }
  }
  return dept;
}

過長(zhǎng)的參數(shù)

過長(zhǎng)參數(shù)是不合理的,通常超過3個(gè)就要謹(jǐn)慎,超過5個(gè)要被禁止。如果一個(gè)方法有一個(gè)長(zhǎng)長(zhǎng)的參數(shù),首先考慮的不是通過把多個(gè)參數(shù)封裝在一起。

如果一個(gè)方法需要多個(gè)參數(shù)不能從自己的宿主類獲得,那么這個(gè)方法的位置可能有問題,要警惕這點(diǎn)。


雙胞胎Class.forName

Class.forName:返回與給定的字符串名稱相關(guān)聯(lián)類或接口的Class對(duì)象

該方法有兩種形式:

Class.forName(String name, boolean initialize, ClassLoader loader)

  • name表示的是類的全名
  • initialize表示是否初始化類
  • loader表示加載時(shí)使用的類加載器

Class.forName(String className)

等價(jià)于:Class.forName(name, true, this.getClassLoader)


鏈接的有效性測(cè)試

Web開發(fā)中頁面與控制器的映射通常是使用配置完成。

靜態(tài)語言一大優(yōu)勢(shì)是編譯期的檢查,幫助我們重構(gòu)代碼,發(fā)現(xiàn)錯(cuò)誤。但無法檢查配置是否正確,例如:

在維護(hù)Struct1項(xiàng)目過程中我們有一個(gè)基本的抽象ActionExtendAction提供了頁面CRUD訪問的入口,多數(shù)的Action都繼承ExtendAction ,現(xiàn)在有個(gè)報(bào)表的抽象類AbstractReportAction也是繼承了ExtendAction。

某次修改希望為報(bào)表Action增加新功能,目前看到需求報(bào)表都沒有使用到CRUD功能,因此擴(kuò)展的方式是通過重構(gòu),引入了新的抽象Action代替了AbstractReportAction的父類ExtendAction

另一個(gè)需求需要使用到CRUD的功能,因?yàn)槔^承的AbstractReportAction是繼承自ExtendAction的可以直接使用。

現(xiàn)在兩個(gè)需求開發(fā)完成,分別在自己分支上測(cè)試完成,合并代碼后,編譯正確。

如果合并后,結(jié)果依賴分支測(cè)試,沒有充分集成測(cè)試,上線后可能就會(huì)造成事故。

改進(jìn):

  1. 少用繼承,這次的案例中一定程度是因?yàn)槎鄬哟蔚睦^承關(guān)系隱蔽了錯(cuò)誤;
  2. 對(duì)基礎(chǔ)類的修改,需要謹(jǐn)慎,團(tuán)隊(duì)的所有成員需要對(duì)此保持警惕,細(xì)心評(píng)估可能現(xiàn)階段及未來可能帶來的風(fēng)險(xiǎn)。
  3. 對(duì)鏈接的有效性測(cè)試,在單元測(cè)試層面添加測(cè)試是非常有必要的。
  4. 除了單元測(cè)試,當(dāng)多個(gè)功能集成后,集成測(cè)試也很關(guān)鍵。

單元測(cè)試框架補(bǔ)充——Junit

平時(shí)對(duì)JUnit使用比較簡(jiǎn)單,今天看到一個(gè)介紹Junit的:

  • JUnit中的Assert方法:

    • void assertEquals(boolean expected, boolean actual)檢查兩個(gè)變量或者等式是否平衡

    • void assertFalse(boolean condition)檢查條件是假的

    • void assertNotNull(Object object)檢查對(duì)象不是空的

    • void assertNull(Object object)檢查對(duì)象是空的

    • void assertTrue(boolean condition)檢查條件為真

    • void fail()在沒有報(bào)告的情況下使測(cè)試不通過

  • JUnit中的注解

    • @BeforeClass:針對(duì)所有測(cè)試,只執(zhí)行一次,且必須為static void
    • @Before:初始化方法
    • @Test:測(cè)試方法,在這里可以測(cè)試期望異常和超時(shí)時(shí)間
    • @After:釋放資源
    • @AfterClass:針對(duì)所有測(cè)試,只執(zhí)行一次,且必須為static void
    • @Ignore:忽略的測(cè)試方法
  • 時(shí)間測(cè)試

    @Test(timeout = 1000)
    public void testTimeoutSuccess() {
        // do nothing
    }
    
  • 異常測(cè)試

    @Test(expected = NullPointerException.class)
    public void testException() {
        throw new NullPointerException();
    }
    

反射中的isAccessible

isAccessible()返回值的含義:

true:直接訪問,不受檢查

false:不能直接訪問,接受檢查

所有的accessible標(biāo)志默認(rèn)都為false,哪怕它使public的。

setAccessible設(shè)置是否開啟直接訪問(關(guān)閉安全檢查),反射調(diào)用提升速度很關(guān)鍵。

這里很容易誤解成,private 的成員調(diào)用 isAccessible 返回 true,public 的成員調(diào)用 isAccessible 返回 false。

舉個(gè)例子:

public class Employee {
    private String id;
    public String name;
    // 省略構(gòu)造,toString()
}

public class AccessibleTest {
    public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException {
        Employee employee = new Employee("1", "ZS");
        System.out.println(employee);
        Class<?> clazz = employee.getClass();
        printFieldAccess(clazz, "id");
        // id isPublic:false    id isAccessible:false
        printFieldAccess(clazz, "name");
        // name isPublic:true    name isAccessible:false
        changeFieldValue(employee, "name");
        // Employee{id='1', name='TEST'}
        changeFieldValue(employee, "id");
        // java.lang.IllegalAccessException
    }

    private static void printFieldAccess(Class<?> clazz, String fieldName) throws NoSuchFieldException {
        Field field = clazz.getDeclaredField(fieldName);
        System.out.println(field.getName() + " isPublic:" + Modifier.isPublic(field.getModifiers()));
        System.out.println(field.getName() + " isAccessible:" + field.isAccessible() + "\n");
    }

    private static void changeFieldValue(Object object, String fieldName) throws NoSuchFieldException, IllegalAccessException {
        Class clazz = object.getClass();
        Field field = clazz.getDeclaredField(fieldName);
        field.set(object, "TEST");
        System.out.println(object);
    }
}

反射API中的兄弟方法

反射API中有幾對(duì)兄弟,可以概括為getXxx()/getDeclaredXxx()

getXxx() :獲取所有的公有Xxx,包括繼承來的

getDeclaredXxx:獲取“當(dāng)前類“的所有Xxx,不受訪問權(quán)限限制


基本/包裝類型在反射中坑

JDK5之后基本類型與包裝類型之間轉(zhuǎn)換已經(jīng)可以做到自動(dòng)拆箱/裝箱了,日常開發(fā)不再留意這些,但是反射中還是要自己注意類型,例如:

public class ReflectBoxingTest {
    private static class Employee {
        public void testBaeType(int x) {
        }

        public void testBoxingType(Integer x) {
        }
    }

    public static void main(String[] args) {
        Class<Employee> clazz = Employee.class;
        getMethod(clazz, "testBaeType", int.class);
        getMethod(clazz, "testBaeType", Integer.class);
        getMethod(clazz, "testBoxingType", int.class);
        getMethod(clazz, "testBoxingType", Integer.class);
    }

    private static void getMethod(Class<?> clazz, String method, Class<?>...classes) {
        try {
            clazz.getDeclaredMethod(method, classes);
        } catch (NoSuchMethodException e) {
            System.out.println("can't get method " + method 
                               + "(" + Arrays.toString(classes) + ")");
        }
    }
}

很容易讓人覺得上述四種方式均能過去到method,實(shí)際上int.classInteger.class之間是不能自動(dòng)轉(zhuǎn)型的。

輸出結(jié)果:

can't get method testBaeType([class java.lang.Integer])
can't get method testBoxingType([int])

引申:在ORM框架中的問題等等。


謹(jǐn)慎使用DB中not-null="false"

如果對(duì)一個(gè)字段設(shè)置了not-null=true,那么在以后的使用當(dāng)中無論是發(fā)送SQL語句還是處理實(shí)體類,都必須先對(duì)其作出判空處理。例子:

新添加了一個(gè)字段期望管理員限定是否只在移動(dòng)端訪問

Hibernate配置文件中:

<property
          name="fdMobileOnly"
          column="fd_mobile_only"
          update="true"
          insert="true"
          not-null="false"
          length="1" />

HQL語句:

where = ....
where += " and entity.fdMobileOnly != 1";

代碼發(fā)布后,管理員沒有第一時(shí)間更新所有字段,用戶訪問后出錯(cuò),當(dāng)然這不是管理員的錯(cuò)。這樣的錯(cuò)誤在開發(fā)階段是可以避免的,測(cè)試階段,發(fā)布階段都可以做些什么,但是我們?nèi)鄙傧鄳?yīng)的支持。


內(nèi)省

比反射更便捷的操作 bean 實(shí)例,使用內(nèi)省機(jī)制。內(nèi)省Demo


異常處理

盡可能的簡(jiǎn)單,復(fù)雜的異常處理過程本身就可能包含了新的異常,在異常處理中發(fā)生新的異??赡芫蜁?huì)覆蓋原始異常,這不是我們想看到的。對(duì)業(yè)務(wù)的異常增加 warn 日志,非業(yè)務(wù)的異常增加 error 日志。


Thread.run()/start()

start():用于啟動(dòng)線程

run():普通方法

直接調(diào)用run方法將不會(huì)開啟新的線程,依舊是順序執(zhí)行。


小數(shù)問題

System.out.println(0.1f+0.1f); // 0.2

System.out.println(0.1f*0.1f); // 0.010000001

第一行結(jié)果接近0.2所以輸出時(shí)系統(tǒng)選擇輸出了0.2


泛型問題

Java實(shí)現(xiàn)泛型是使用泛型擦除的

Java的泛型不能協(xié)變

可以使用通配符指定不確定的泛型

  • 生產(chǎn)者使用extends
    • <? extends T> 聲明的類型是T的子類,具體哪個(gè)子類不知道,所以不能隨便給?賦引用,因?yàn)榭赡艹霈F(xiàn)轉(zhuǎn)型錯(cuò)誤,但是知道是T的子類,肯定實(shí)現(xiàn)了T的方法
  • 消費(fèi)者使用super
    • <? super T> 聲明的類型是T的父類,具體哪個(gè)父類不知道,反正至少T類型,所以T的子類型都可以被?引用,但引用的是什么類型不知道,所以根據(jù)?獲取的都是object

線程創(chuàng)建

在Java中創(chuàng)建線程,一個(gè)線程默認(rèn)就會(huì)預(yù)留1M的空間,那么1G的內(nèi)存也不過只能支持1000個(gè)線程創(chuàng)建而已


獲取泛型類型T的類實(shí)例


import org.springframework.core.GenericTypeResolver;
public abstract class AbstractHibernateDao<T extends DomainObject> implements DataAccessObject<T>
{
@Autowired
private SessionFactory sessionFactory;
private final Class<T> genericType;
private final String RECORD_COUNT_HQL;
private final String FIND_ALL_HQL;
@SuppressWarnings("unchecked")
public AbstractHibernateDao()
{
 this.genericType = (Class<T>) GenericTypeResolver.resolveTypeArgument(getClass(), AbstractHibernateDao.class);
 this.RECORD_COUNT_HQL ="select count(*) from" + this.genericType.getName();
 this.FIND_ALL_HQL ="from" + this.genericType.getName() +" t";
}


ThreadPoolExecutor.submit()/execute()

ThreadPoolExecutor 線程池中 submit 和 execute 方法對(duì)比

  1. submit 提交的任務(wù)可以獲取執(zhí)行結(jié)果,而execute 則不能
  2. execute 會(huì)拋出異常,而submit 如果不獲取執(zhí)行結(jié)果的話會(huì)“吞掉”異常

詳細(xì)分析:http://blog.techbeta.me/2016/08/ThreadPoolExecutor/


ThreadPoolExecutor 線程池對(duì)異常處理

  1. 捕獲 execute 拋出異常
  2. submit 執(zhí)行完成必須 get 一下檢測(cè)異常
  3. 擴(kuò)展線程池,實(shí)現(xiàn) beforeExecute 方法

轉(zhuǎn)型問題

即使是同一個(gè)類文件,如果是由不同的類加載器實(shí)例加載的,那么它們的類型是不相同的。例如:

public void run(){ 
    try { 
        // 每次都創(chuàng)建出一個(gè)新的類加載器
        HowswapCL cl = new HowswapCL("../swap", new String[]{"Foo"}); 
        Class cls = cl.loadClass("Foo"); 
        Object foo = cls.newInstance(); 
 
        Method m = foo.getClass().getMethod("sayHello", new Class[]{}); 
        m.invoke(foo, new Object[]{}); 
     
    }  catch(Exception ex) { 
        ex.printStackTrace(); 
    } 
}

cls 是由 HowswapCL 加載的,而 foo 變量類型聲名和轉(zhuǎn)型里的 Foo 類卻是由 run 方法所屬的類的加載器(默認(rèn)為 AppClassLoader)加載的,因此是完全不同的類型,所以會(huì)拋出轉(zhuǎn)型異常

解決:
http://www.wisedream.net/2017/01/17/programming/type-cast-across-classloader/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容