2013年9月11日 星期三

Spring Security 建立基本登入流程及模擬SSO登入

今天要和大家分享一個Spring所提供的一個權限控管的framework,SpringSecurity,這是Spring 所推出可以針對下列功能做出客製化設定:
  1. 會員權限設定
  2. 功能權限設定
  3. SSO(Single Sing On)
  4. 會員登入功能
上述功能為我個人專案中常用的需求功能,當然還有提供別種設定,詳細請見官網

今天來實作一個SpringSecurity的範例,詳細的設定說明直接寫在設定檔,可以至git進行下載


1. 環境設定:

  1. Maven 3
  2. Eclipse 4.2
  3. JDK 1.6
  4. Spring Core 3.2.2.RELEASE
  5. Spring Batch 2.2.0.RELEASE
  6. MySQL Java Driver 5.1.25

2.table建立:

CREATE TABLE `users` (
  `id` int(10) unsigned NOT NULL,
  `username` varchar(45) NOT NULL,
  `password` varchar(45) NOT NULL,
  `enable` tinyint(1) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `user_roles` (
  `id` int(10) unsigned NOT NULL,
  `user_id` int(10) unsigned NOT NULL,
  `authority` varchar(45) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `FK_user_roles` (`user_id`),
  CONSTRAINT `FK_user_roles` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


3.mavan pom
    
        <properties>
  <spring .version="">3.0.5.RELEASE</spring>
 </properties>
 <dependencies>

  <!-- Spring 3 -->
  <dependency>
   <groupid>org.springframework</groupid>
   <artifactid>spring-core</artifactid>
   <version>${spring.version}</version>
  </dependency>

  <dependency>
   <groupid>org.springframework</groupid>
   <artifactid>spring-web</artifactid>
   <version>${spring.version}</version>
  </dependency>

  <dependency>
   <groupid>org.springframework</groupid>
   <artifactid>spring-webmvc</artifactid>
   <version>${spring.version}</version>
  </dependency>

  <dependency>
   <groupid>org.springframework</groupid>
   <artifactid>spring-jdbc</artifactid>
   <version>${spring.version}</version>
  </dependency>


  <!-- Spring Security -->
  <dependency>
   <groupid>org.springframework.security</groupid>
   <artifactid>spring-security-core</artifactid>
   <version>${spring.version}</version>
  </dependency>

  <dependency>
   <groupid>org.springframework.security</groupid>
   <artifactid>spring-security-web</artifactid>
   <version>${spring.version}</version>
  </dependency>

  <dependency>
   <groupid>org.springframework.security</groupid>
   <artifactid>spring-security-config</artifactid>
   <version>${spring.version}</version>
  </dependency>

  <dependency>
   <groupid>org.springframework.security</groupid>
   <artifactid>spring-security-taglibs</artifactid>
   <version>${spring.version}</version>
  </dependency>
  <dependency>
   <groupid>javax.servlet</groupid>
   <artifactid>servlet-api</artifactid>
   <version>2.5</version>
  </dependency>

  <dependency>
   <groupid>jstl</groupid>
   <artifactid>jstl</artifactid>
   <version>1.2</version>
  </dependency>


  <dependency>
   <groupid>org.apache.commons</groupid>
   <artifactid>commons-lang3</artifactid>
   <version>3.0</version>
  </dependency>
  
  <dependency>
   <groupid>mysql</groupid>
   <artifactid>mysql-connector-java</artifactid>
   <version>5.1.20</version>
  </dependency>

4.spring security 設定檔

  spring-security.xml :


<beans:beans xmlns="http://www.springframework.org/schema/security"
 xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.springframework.org/schema/beans
 http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
 http://www.springframework.org/schema/security
 http://www.springframework.org/schema/security/spring-security-3.0.3.xsd">

    <!--
    
               說明:
         <http> 設定區塊主要是要設定filter 及 因為需求需要被springSecurity 監控的地方,
                       並且可以設定filter chain 相關順序以及登入及登出預設url,可利用spring bean 特性
                       針對因需求而客製化的filter。
                       
               設定說明:
         1.auto-config="true" spring 會依照預設的filter chain 進行注入動作 ,當然也可以使用自訂filter進行
          1.1 文件說明:
            Automatically registers a login form, BASIC authentication, anonymous authentication, logout services, 
   remember-me and servlet-api-integration. If set to "true", all of these capabilities are added (although 
   you can still customize the configuration of each by providing the respective element). If unspecified, 
   defaults to "false".
    1.2 filter chain 文件說明:
       http://static.springsource.org/spring-security/site/docs/3.0.x/reference/ns-config.html - 2.3.5 Adding in Your Own Filters
         
         2.entry-point-ref 登入進入點 此設定如果沒設定,則<form-login login-page=""/> 必須要設定,兩者擇一。
         
         3.intercept-url 針對所有request filter 需要做權限控管部分作客製化設定,此部分我不直接導向頁面,因為如果直接導向頁面,
                             則必須將相關頁面至於可由外部url直接讀取位置,有安全性顧慮,因此統一至於WEB-INF目錄下,並使用spring controller 進行forward。
         
         4.access-denied-page 當使用者登入後,如權限不足以瀏覽所設定的位址,則導向設定頁面,如沒設定,則拋出403錯誤代碼。
          4.1 IS_AUTHENTICATED_ANONYMOUSLY  如果使用者沒有經過正式權限授予則會透過AnonymousAuthenticationFilter自動給予一個ANONYMOUSLY權限。
            signifies that anyone can access this URL. By default the AnonymousAuthenticationFilter ensures an 'anonymous' Authentication with no roles so that every user has an authentication. The token accepts any authentication, even anonymous.
          4.2 IS_AUTHENTICATED_FULLY 使用者具備權限即可
            requires the user to be fully authenticated with an explicit login.
         
         5.custom-filter  position="PRE_AUTH_FILTER" 依照 http://static.springsource.org/spring-security/site/docs/3.0.x/reference/ns-config.html
                             中說明PRE_AUTH_FILTER 為一種透過內部系統進行驗證後並且透過request data such as headers,進行登入動作,而ref對象為繼承AbstractPreAuthenticatedProcessingFilter
                             子類別。
           5.1 文件說明:
           The purpose is then only to extract the necessary information on the principal from the incoming request, 
           rather than to authenticate them. External authentication systems may provide this information via request data such as headers or cookies which the pre-authentication system can extract. 
           It is assumed that the external system is responsible for the accuracy of the data and preventing the submission of forged values
                            上述已經說明此filter主要是用來取得必要資訊,而並非是要驗證資訊,此filter獲得資訊是假定內部系統所傳送的資訊是有防止變造能力的系統,也就是在信任狀況下進行後續動作
           By default, the filter chain will proceed when an authentication attempt fails in order to allow other authentication mechanisms to process the request. To reject the credentials immediately, 
           set the continueFilterChainOnUnsuccessfulAuthentication flag to false. The exception raised by the AuthenticationManager will the be re-thrown
                             預設中如沒得到相關資訊程序會進行下去並透過類似的機制進行後續驗證處理,但如果系統中透過內部登入機制不允許繼續進行流程,則需要將 continueFilterChainOnUnsuccessfulAuthentication設定為false。
         
         6.form-login 此設定為自動會讓UsernamePasswordAuthenticationFilter加入filter chain,但是  <custom-filter position="FORM_LOGIN_FILTER" ref="loginFilter" /> 就無法使用
                             因為form-login已經率先占用此位置,所以如果有需要客製化 UsernamePasswordAuthenticationFilter 並且取代原本位置 並且  auto-config="false" 
           6.1 文件說明:
           Note that you can still use auto-config. The form-login element just overrides the default settings. Also note that we've added an extra intercept-url element to say that any requests for the login page should be available to anonymous users [5]. 
           Otherwise the request would be matched by the pattern /** and it wouldn't be possible to access the login page itself! This is a common configuration error and will result in an infinite loop in the application. Spring Security will emit a warning in the log if your login page appears to be secured. 
           It is also possible to have all requests matching a particular pattern bypass the security filter chain completely:
                             這段有提到一個比較有趣的狀況,就是例如很多時候很多人設定了<form-login login-page='/login'/> 但是卻沒有設定<intercept-url pattern="/login*" filters="none"/>
                             這是一個很常見的錯誤,會造成無窮迴圈。因為當filter發現使用者未登入的時候會想要把使用者導向login.jsp,但是由於沒設定所以會導致存取login.jsp又被導向login.jsp這樣無窮迴圈
         
         7.補充,auto-config設定為true,則會加入 BASIC authentication, anonymous authentication, logout services, remember-me and servlet-api-integration 四種
                             所以如果並非上述四種filter,則加入其他自訂filter則無須理會auto-config設定為true造成衝突的狀況
           7.1 文件說明:
           Automatically registers a login form, BASIC authentication, anonymous authentication, logout services, 
     remember-me and servlet-api-integration. If set to "true", all of these capabilities are added (although 
     you can still customize the configuration of each by providing the respective element). If unspecified, 
     defaults to "false".
                                     
      -->
 <http auto-config="true" entry-point-ref="loginUrlEntryPoint"
  access-denied-page="/accessDenied">
  <intercept-url pattern="/login" access="IS_AUTHENTICATED_ANONYMOUSLY" />
  <intercept-url pattern="/expire" access="IS_AUTHENTICATED_ANONYMOUSLY" />
  <intercept-url pattern="/accessDenied" access="IS_AUTHENTICATED_ANONYMOUSLY" />
  <intercept-url pattern="/welcome" access="IS_AUTHENTICATED_FULLY" />
  <intercept-url pattern="/logout" filters="none" />
  <intercept-url pattern="/css/**" filters="none" />
  <intercept-url pattern="/**" access="ROLE_ADMIN,ROLE_USER,ROLE_CUS_USER" />
  <custom-filter position="PRE_AUTH_FILTER" ref="siteminderFilter" />
  <custom-filter position="CONCURRENT_SESSION_FILTER" ref="concurrencyFilter" />
  <custom-filter before="FORM_LOGIN_FILTER" ref="loginFilter" />
  <logout logout-success-url="/logout" />
  <form-login 
   authentication-failure-url="/login"
   authentication-success-handler-ref="authenticationSuccessHandler" />
  <session-management session-authentication-strategy-ref="sas" invalid-session-url="/invalid"/>
 </http>

     <!-- 
        1.說明:此filter主要是用來處理兩種情況
         1.1 透過sessionRegistry取得session 相關資訊,如判斷過期則進行過期導頁程序
         1.2 透過sessionRegistry取得相關session資訊,如沒過期,則重新更新到期日期資訊。
        2.sessionRegistry 通常預設都是使用SessionRegistryImpl進行
     -->
 <beans:bean id="concurrencyFilter"
  class="org.springframework.security.web.session.ConcurrentSessionFilter">
  <beans:property name="sessionRegistry" ref="sessionRegistry" />
  <beans:property name="expiredUrl" value="/expire" />
 </beans:bean>
     <!-- 
        1.說明:此filter是Override UsernamePasswordAuthenticationFilter
         1.1 UsernamePasswordAuthenticationFilter
         1.2 透過sessionRegistry取得相關session資訊,如沒過期,則重新更新到期日期資訊。
        2.authenticationManager 驗證使用者資料的主要驗證程式 。
        3.filterProcessesUrl 設定驗證參數 ,主要用來判斷request URL 中是否還含有j_spring_security_check,用來辨別request 為驗證要求。
     -->
 <beans:bean id="loginFilter"
  class="com.bt.filter.BtUsernamePasswordAuthenticationFilter">
  <beans:property name="authenticationManager" ref="authenticationManager" />
  <beans:property name="authenticationFailureHandler"
   ref="authenticationFailureHandler" />
  <beans:property name="filterProcessesUrl" value="/j_spring_security_check" />
 </beans:bean>

    <!-- 1.說明 : UsernamePasswordAuthenticationFilter 驗證成功後,透過此handler進行後續session資料儲存及導頁
          1.1:defaultTargetUrl 和<http> 中的 <form-login> login-processing-url 效果相同,兩者可以擇一 
    -->
 <beans:bean id="authenticationSuccessHandler"
  class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
  <beans:property name="defaultTargetUrl" value="/welcome" />
 </beans:bean>

 <beans:bean id="authenticationFailureHandler"
  class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
  <beans:property name="defaultFailureUrl" value="/login" />
 </beans:bean>

 <beans:bean id="sessionRegistry"
  class="org.springframework.security.core.session.SessionRegistryImpl" />
  
 <!-- 
    1.說明:主要是要判斷是否每一個user登入的session數量超過所設定的上限,預設值為1
     1.1 文件說明:Strategy which handles concurrent session-control, in addition to the functionality provided by the base class. When invoked following an authentication, it will check whether the user in question should be allowed to proceed,
         by comparing the number of sessions they already have active with the configured maximumSessions value. The SessionRegistry is used as the source of data on authenticated users and session data.
            If a user has reached the maximum number of permitted sessions, the behaviour depends on the exceptionIfMaxExceeded property. The default behaviour is to expired the least recently used session, 
            which will be invalidated by the ConcurrentSessionFilter if accessed again. If exceptionIfMaxExceeded is set to true, however, the user will be prevented from starting a new authenticated session.
                     如已經有使用者超過登入上限數量,則會將前一個用戶的session expire。如exceptionIfMaxExceeded = true,則不會踢掉前一個user session,而會透過SessionManagementFilter
                     進行導頁而無法登入
  --> 
 <beans:bean id="sas"
  class="org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy">
  <beans:constructor-arg name="sessionRegistry"
   ref="sessionRegistry" />
  <beans:property name="exceptionIfMaximumExceeded"
   value="true" />
  <beans:property name="maximumSessions" value="1" />
 </beans:bean>
    <!--  
        1.說明:SSO登入主要filter,透過header資訊進行登入
          1.1.文件說明:As with most pre-authenticated scenarios, it is essential that the external authentication system is set up correctly as this filter does no authentication whatsoever. 
              All the protection is assumed to be provided externally and if this filter is included inappropriately in a configuration, it would be possible to assume the identity of a user merely by setting the correct header name.
              This also means it should not generally be used in combination with other Spring Security authentication mechanisms such as form login, as this would imply there was a means of bypassing the external system which would be risky.
                        文件中闡述了幾個要點,就是使用此filter是假定是使用內部系統進行保護並且額外透過CA Siteminder來進行header 資料的給予,如設定檔中有不恰當的設定,有可能會導致
         header資訊被任意竄改,造成安全性問題,同時官方也建議如已經使用此filter進行登入,則不要混用其他登入機制,例如login form,以確保安全。
        2.參數說明:
          principalRequestHeader:設定header標頭名稱,SM_USER為預設值
          exceptionIfHeaderMissing:如沒傳入principalRequestHeader值則拋出例外
          authenticationManager:提供給RequestHeaderAuthenticationFilter獲得使用者資料用
    -->
 <beans:bean id="siteminderFilter"
  class="org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter">
  <beans:property name="principalRequestHeader" value="SM_USER" />
  <beans:property name="exceptionIfHeaderMissing" value="false" />
  <beans:property name="authenticationManager" ref="authenticationManager" />
 </beans:bean>
   <!-- 
                    說明:RequestHeaderAuthenticationFilter 透過 PreAuthenticatedAuthenticationProvider 取得授權資料
        preAuthenticatedUserDetailsService:用來取得授權資料
    -->
 <beans:bean id="preauthAuthProvider"
  class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
  <beans:property name="preAuthenticatedUserDetailsService">
   <beans:bean id="userDetailsServiceWrapper"
    class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
    <beans:property name="userDetailsService" ref="userDetailsService" />
   </beans:bean>
  </beans:property>
 </beans:bean>

 <beans:bean id="usernamePasswordProvider"
  class="com.bt.security.provider.BTCustomerAuthenticationProvider">
  <beans:property name="userDetailsService" ref="userDetailsService" />
  <beans:property name="passwordEncoder" ref="md5PasswordEncoder" />
 </beans:bean>

 <beans:bean id="md5PasswordEncoder"
  class="org.springframework.security.authentication.encoding.Md5PasswordEncoder" />

 <authentication-manager alias="authenticationManager">
  <authentication-provider ref="preauthAuthProvider" />
  <authentication-provider ref="usernamePasswordProvider" />
 </authentication-manager>

 <beans:bean id="userDetailsService" class="com.bt.security.BTUserDetailService" />

 <beans:bean id="loginUrlEntryPoint"
  class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
  <beans:property name="loginFormUrl" value="/login" />
 </beans:bean>


</beans:beans>

mvc-dispatcher-servlet.xml :
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:context="http://www.springframework.org/schema/context"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="
        http://www.springframework.org/schema/beans     
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context-3.0.xsd">

 <context:component-scan base-package="com.bt" />

 <!-- i18n -->

 <bean id="messageSource"
  class="org.springframework.context.support.ResourceBundleMessageSource">
  <property name="basename" value="messages"></property>
 </bean>

 <bean id="localeResolver"
  class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
  <property name="defaultLocale" value="zh" />
 </bean>

 <bean
  class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  <property name="prefix">
   <value>/WEB-INF/pages/</value>
  </property>
  <property name="suffix">
   <value>.jsp</value>
  </property>
 </bean>
</beans>

5.程式重點範例:


package com.bt.filter;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.bt.services.ILoginBO;

public class BtUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
 @Autowired
 ILoginBO  loginBO;
 private String principalRequestHeader = "SM_USER";

 @Override
 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
  final HttpServletRequest request = (HttpServletRequest) req;
  String sUserId = request.getHeader(principalRequestHeader);
  if (StringUtils.isBlank(sUserId)) {
   sUserId = request.getParameter("j_username");
   if (StringUtils.isNotBlank(sUserId)) {
    request.getSession().setAttribute(principalRequestHeader, sUserId);
   }
   super.doFilter(req, res, chain);
  }
  else {
   final String iv_user = (String) request.getSession().getAttribute(principalRequestHeader);
   if (StringUtils.isBlank(iv_user)) {
    request.getSession().setAttribute("iv-user", sUserId);
   }
   chain.doFilter(req, res);
  }
 }
}


    我主要overwrite UsernamePasswordAuthenticationFilter 部分,從程式裡可以明顯看出SSO登入    部分的key為SM_USER,此部分資訊是從header傳入,如沒傳入則透過一般登入流程。

  6.結論:
    5.1  上述範例只擷取部分重要的設定,完整示範檔案可至git下載.
    5.2  次篇最重要的部分為設定檔案註解部分,如有錯誤的地方歡迎指證討論。

2013年7月1日 星期一

ArrayList,LinkedList 查詢及修改效能比較

在專案中常常會用到Collection的ArrayList,LinkedList這兩種,這兩種在使用者或許沒有多大不同,但是在本質上卻有很大的不相同,今天來探討這兩者本質部份以及效能部分

1.ArrayList 資料結構















2.LinkedList資料結構



3.1 比較兩者隨機查詢資料效能

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;

public class ListTest {
 //資料大小
 int dataSize = 100000;
 // 紀錄開始時間
 long[]   testBeginTime = new long[4];
 // 紀錄結束時間
 long[]   testEndTime  = new long[4];
 // 基本資料長度
 Integer[]  ia    = new Integer[dataSize];
 // 初始化隨機物件
 Random   random   = new Random();
 // 初始化ArrayList
 List<Integer> arraylist  = null;
 // 初始化linkList
 List<Integer> linklist  = null;
 
 {
  // 初始化ia陣列資料
  for (int i = 0; i < ia.length; i++) {
   ia[i] = i;
  }
  arraylist = new ArrayList<Integer>(Arrays.asList(ia));
  linklist = new LinkedList<Integer>(Arrays.asList(ia));
 }

 void testListRandomQuery() {
  testBaginTime[0] = System.currentTimeMillis();
  for (int i = 0; i < dataSize; i++) {
   arraylist.get(random.nextInt(dataSize));
  }
  testEndTime[0] = System.currentTimeMillis();
  testBaginTime[1] = System.currentTimeMillis();
  for (int i = 0; i < dataSize; i++) {
   linklist.get(random.nextInt(dataSize));
  }
  testEndTime[1] = System.currentTimeMillis();
  System.out.println("ArrayList time : " + ((testEndTime[0] - testBeginTime[0]) / 1000) + " sec");
  System.out.println("LinkedList time : " + ((testEndTime[1] - testBeginTime[1]) / 1000) + " sec");
 };

 void testListModify() {
 };

 public static void main(String[] args) throws InterruptedException {
  new ListTest().testListRandomQuery();
 }
}

執行結果:
ArrayList time : 0 sec
LinkedList time : 7 sec

從執行結果來看,於讀取隨機資料LinkedList 所花的時間足足比ArrayList多花上七秒的時間
這是以十萬筆資料的情況來進行模擬,從這裡可以展現出ArrayList比LinkedList在進行隨機
讀取資料上效能的展現

3.2 比較兩者隨機插入資料效能
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;

public class ListTest {
 //資料大小
 int dataSize = 100000;
 // 紀錄開始時間
 long[]   testBeginTime = new long[4];
 // 紀錄結束時間
 long[]   testEndTime  = new long[4];
 // 基本資料長度
 Integer[]  ia    = new Integer[dataSize];
 // 初始化隨機物件
 Random   random   = new Random();
 // 初始化ArrayList
 List<Integer> arraylist  = null;
 // 初始化linkList
 List<Integer> linklist  = null;
 
 {
  // 初始化ia陣列資料
  for (int i = 0; i < ia.length; i++) {
   ia[i] = i;
  }
  arraylist = new ArrayList<Integer>(Arrays.asList(ia));
  linklist = new LinkedList<Integer>(Arrays.asList(ia));
 }

 void testListModify() {
  testBaginTime[0] = System.currentTimeMillis();
  for (int i = 0; i < dataSize; i++) {
   arraylist.add(random.nextInt(dataSize),i);
  }
  testEndTime[0] = System.currentTimeMillis();
  testBaginTime[1] = System.currentTimeMillis();
  for (int i = 0; i < dataSize; i++) {
   linklist.add(random.nextInt(dataSize),i);
  }
   testEndTime[1] = System.currentTimeMillis();
   System.out.println("ArrayList time : " + ((testEndTime[0] - testBeginTime[0]) / 1000) + " sec");
   System.out.println("LinkedList time : " + ((testEndTime[1] - testBeginTime[1]) / 1000) + " sec");
 };

 public static void main(String[] args) throws InterruptedException {
  new ListTest().testListModify();
 }
}

執行結果:
ArrayList time : 4 sec
LinkedList time : 128 sec
這個實驗數據很重要,因為常常看到網路上文章提到,LinkedList在進行插入動作時候效能
優於ArrayList,但是實驗數據出來差異頗大。

3.3 比較兩者依序插入資料效能


import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;

public class ListTest {
 //資料大小
 int dataSize = 100000;
 // 紀錄開始時間
 long[]   testBaginTime = new long[4];
 // 紀錄結束時間
 long[]   testEndTime  = new long[4];
 // 基本資料長度
 Integer[]  ia    = new Integer[dataSize];
 // 初始化隨機物件
 Random   random   = new Random();
 // 初始化ArrayList
 List<Integer> arraylist  = null;
 // 初始化linkList
 List<Integer> linklist  = null;
 
 {
  // 初始化ia陣列資料
  for (int i = 0; i < ia.length; i++) {
   ia[i] = i;
  }
  arraylist = new ArrayList<Integer>(Arrays.asList(ia));
  linklist = new LinkedList<Integer>(Arrays.asList(ia));
 }

 
 void testListAdd() {
  testBaginTime[0] = System.currentTimeMillis();
  for (int i = 0; i < dataSize; i++) {
   arraylist.add(i,random.nextInt(dataSize));
  }
  testEndTime[0] = System.currentTimeMillis();
  testBaginTime[1] = System.currentTimeMillis();
  for (int i = 0; i < dataSize; i++) {
   linklist.add(i,random.nextInt(dataSize));
  }
  testEndTime[1] = System.currentTimeMillis();
  System.out.println("ArrayList time : " + ((testEndTime[0] - testBaginTime[0]) / 1000) + " sec");
  System.out.println("LinkedList time : " + ((testEndTime[1] - testBaginTime[1]) / 1000) + " sec");
 };

 public static void main(String[] args) throws InterruptedException {
  new ListTest().testListAdd();
 }
}
執行結果:
ArrayList time : 5 sec 
LinkedList time : 44 sec 
這個實驗數據很重要,因為常常看到網路上文章提到,LinkedList在進行插入動作時候效能 是優於ArrayList,但是實驗數據出來差異頗大。

3.4 比較兩者插入資料列首筆資料效能



     //以上類別變數可參考上面測試範例
 void testListHeadAdd(){
  testBaginTime[0] = System.currentTimeMillis();
  for (int i = 0; i < dataSize; i++) {
   arraylist.add(dataSize,random.nextInt(dataSize));
  }
  testEndTime[0] = System.currentTimeMillis();
  testBaginTime[1] = System.currentTimeMillis();
  for (int i = 0; i < dataSize; i++) {
   linklist.add(dataSize,random.nextInt(dataSize));
  }
  testEndTime[1] = System.currentTimeMillis();
  System.out.println("ArrayList time : " + ((testEndTime[0] - testBaginTime[0]) / 1000) + " sec");
  System.out.println("LinkedList time : " + ((testEndTime[1] - testBaginTime[1]) / 1000) + " sec");
 }
執行結果:
ArrayList time : 7 sec 
LinkedList time : 0 sec 

4 .結論:

  • 讀取資料無論是隨機或是循序,使用優先順序為ArrayList > LinkedList
  • 插入資料無論是危機或是循序,使用優先順序為ArrayList > LinkedList
5.個人看法:

      從上面我所列出的資料結構圖形部分,基本的認知就是,當使用arraylist進行資料刪除候,就會產生"牽一髮動全身"的效應,所以感覺起來arraylist比較使用土方法在搬運資料。
      
       而使用linkedlist進行資料刪除時候,只要記憶體指針指向另外一個地方,就完成了看起來很快。

        但是無論使用哪一種的list,刪除元素都必須經過"找尋位置","執行搬運"這兩個動作,但很多時候大家都只看到了"執行搬運"這類的動作,但是缺忽略了"找尋位置"這類記憶體操作更是消耗時間,因此會有上面的測試程式這類的情形發生。

         以上是我個人研究的淺見,歡迎大家互相討論。