談談Spring中的對象跟Bean,你知道Spring怎麼創建對象的嗎?

本系列文章:

讀源碼,我們可以從第一行讀起

你知道Spring是怎麼解析配置類的嗎?

配置類為什麼要添加@Configuration註解?

推薦閱讀:

Spring官網閱讀 | 總結篇

Spring雜談

本系列文章將會帶你一行行的將Spring的源碼吃透,推薦閱讀的文章是閱讀源碼的基礎!

兩個問題

在開始探討源碼前,我們先思考兩個問題:

1、在Spring中,什麼是Bean?跟對象有什麼區別?

通過new關鍵字,反射,克隆等手段創建出來的就是對象。在Spring中,Bean一定是一個對象,但是對象不一定是一個Bean,一個被創建出來的對象要變成一個Bean要經過很多複雜的工序,例如需要被我們的BeanPostProcessor處理,需要經過初始化,需要經過AOPAOP本身也是由後置處理器完成的)等。

2、在創建對象前,Spring還做了其它什麼事情嗎?

我們還是回到流程圖中,其中相關的步驟如下:

在前面的三篇文章中,我們已經分析到了第3-5步的源碼,而如果你對Spring源碼稍有了解的話,就是知道創建對象以及將對象變成一個Bean的過程發生在第3-11步驟中。中間的五步分別做了什麼呢?

1、registerBeanPostProcessors

就像名字所說的那樣,註冊BeanPostProcessor,這段代碼在Spring官網閱讀(八)容器的擴展點(三)(BeanPostProcessor)已經分析過了,所以在本文就直接跳過了,如果你沒有看過之前的文章也沒有關係,你只需要知道,在這裏Spring將所有的BeanPostProcessor註冊到了容器中

2、initMessageSource

初始化容器中的messageSource,如果程序員沒有提供,默認會創建一個org.springframework.context.support.DelegatingMessageSource,Spring官網閱讀(十一)ApplicationContext詳細介紹(上) 已經介紹過了。

3、initApplicationEventMulticaster

初始化事件分發器,如果程序員沒有提供,那麼默認創建一個org.springframework.context.event.ApplicationEventMulticaster,Spring官網閱讀(十二)ApplicationContext詳解(中)已經做過詳細分析,不再贅述

4、onRefresh

留給子類複寫擴展使用

5、registerListeners

註冊事件監聽器,就是將容器中所有實現了org.springframework.context.ApplicationListener接口的對象放入到監聽器的集合中。

創建對象的源碼分析

在完成了上面的一些準備工作后,Spring開始來創建Bean了,按照流程,首先被調用的就是finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory)方法,我們就以這個方法為入口,一步步跟蹤源碼,看看Spring中的Bean到底是怎麼創建出來的,當然,本文主要關注的是創建對象的這個過程,對象變成Bean的流程我們在後續文章中再分析

1、finishBeanFactoryInitialization

protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
   // 初始化一個ConversionService用於類型轉換,這個ConversionService會在實例化對象的時候用到
   if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
         beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
      beanFactory.setConversionService(
            beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
   }

  // 添加一個StringValueResolver,用於處理佔位符,可以看到,默認情況下就是使用環境中的屬性值來替代佔位符中的屬性
   if (!beanFactory.hasEmbeddedValueResolver()) {
      beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
   }

   // 創建所有的LoadTimeWeaverAware
   String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
   for (String weaverAwareName : weaverAwareNames) {
      getBean(weaverAwareName);
   }

   // 靜態織入完成后將臨時的類加載器設置為null,所以除了創建LoadTimeWeaverAware時可能會用到臨時類加載器,其餘情況下都為空
   beanFactory.setTempClassLoader(null);

   // 將所有的配置信息凍結
   beanFactory.freezeConfiguration();

   // 開始進行真正的創建
   beanFactory.preInstantiateSingletons();
}

上面的方法最終調用了org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons來創建Bean。

其源碼如下:

2、preInstantiateSingletons

public void  preInstantiateSingletons() throws BeansException {
    	// 所有bd的名稱 
		List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
    	// 遍歷所有bd,一個個進行創建 
		for (String beanName : beanNames) {
            // 獲取到指定名稱對應的bd
			RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
            // 對不是延遲加載的單例的Bean進行創建
			if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
                // 判斷是否是一個FactoryBean
				if (isFactoryBean(beanName)) {
                    // 如果是一個factoryBean的話,先創建這個factoryBean,創建factoryBean時,需要在beanName前面拼接一個&符號
					Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
					if (bean instanceof FactoryBean) {
						final FactoryBean<?> factory = (FactoryBean<?>) bean;
						boolean isEagerInit;
						if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
							isEagerInit = AccessController.doPrivileged((PrivilegedAction<Boolean>)
											((SmartFactoryBean<?>) factory)::isEagerInit,
									getAccessControlContext());
						}
						else {
                            // 判斷是否是一個SmartFactoryBean,並且不是懶加載的,就意味着,在創建了這個factoryBean之後要立馬調用它的getObject方法創建另外一個Bean
							isEagerInit = (factory instanceof SmartFactoryBean &&
									((SmartFactoryBean<?>) factory).isEagerInit());
						}
						if (isEagerInit) {
							getBean(beanName);
						}
					}
				}
				else {
                    // 不是factoryBean的話,我們直接創建就行了
					getBean(beanName);
				}
			}
		}
		// 在創建了所有的Bean之後,遍歷
		for (String beanName : beanNames) {
            // 這一步其實是從緩存中獲取對應的創建的Bean,這裏獲取到的必定是單例的 
			Object singletonInstance = getSingleton(beanName);
            // 判斷是否是一個SmartInitializingSingleton,最典型的就是我們之前分析過的EventListenerMethodProcessor,在這一步完成了對已經創建好的Bean的解析,會判斷其方法上是否有	@EventListener註解,會將這個註解標註的方法通過EventListenerFactory轉換成一個事件監聽器並添加到監聽器的集合中
			if (singletonInstance instanceof SmartInitializingSingleton) {
				final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
				if (System.getSecurityManager() != null) {
					AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
						smartSingleton.afterSingletonsInstantiated();
						return null;
					}, getAccessControlContext());
				}
				else {
					smartSingleton.afterSingletonsInstantiated();
				}
			}
		}
	}

上面這段代碼整體來說應該不難,不過它涉及到了一個點就是factoryBean,如果你對它不夠了解的話,請參考我之前的一篇文章:Spring官網閱讀(七)容器的擴展點(二)FactoryBean

3、doGetBean

從上面的代碼分析中我們可以知道,Spring最終都會調用到getBean方法,而getBean並不是真正幹活的,doGetBean才是。另外doGetBean可以分為兩種情況

  • 創建的是一個FactoryBean,此時實際傳入的name = & + beanName
  • 創建的是一個普通Bean,此時傳入的name = beanName

其代碼如下:

	protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
			@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
		// 前面我們說過了,傳入的name可能時& + beanName這種形式,這裏做的就是去除掉&,得到beanName
		final String beanName = transformedBeanName(name);
		Object bean;
		// 這個方法就很牛逼了,通過它解決了循環依賴的問題,不過目前我們只需要知道它是從單例池中獲取已經創建的Bean即可,循環依賴後面我單獨寫一篇文章
        // 方法作用:已經創建的Bean會被放到單例池中,這裏就是從單例池中獲取
		Object sharedInstance = getSingleton(beanName);
        
		if (sharedInstance != null && args == null) {
            // 如果直接從單例池中獲取到了這個bean(sharedInstance),我們能直接返回嗎?
            // 當然不能,因為獲取到的Bean可能是一個factoryBean,如果我們傳入的name是 & + beanName 這種形式的話,那是可以返回的,但是我們傳入的更可能是一個beanName,那麼這個時候Spring就還需要調用這個sharedInstance的getObject方法來創建真正被需要的Bean
			bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
		}
		else {
            // 在緩存中獲取不到這個Bean
            // 原型下的循環依賴直接報錯
			if (isPrototypeCurrentlyInCreation(beanName)) {
				throw new BeanCurrentlyInCreationException(beanName);
			}
            
            // 核心要義,找不到我們就從父容器中再找一次
			BeanFactory parentBeanFactory = getParentBeanFactory();
			if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
				String nameToLookup = originalBeanName(name);
				if (parentBeanFactory instanceof AbstractBeanFactory) {
					return ((AbstractBeanFactory) parentBeanFactory).doGetBean(
							nameToLookup, requiredType, args, typeCheckOnly);
				}
				else if (args != null) {
					return (T) parentBeanFactory.getBean(nameToLookup, args);
				}
				else if (requiredType != null) {
					return parentBeanFactory.getBean(nameToLookup, requiredType);
				}
				else {
					return (T) parentBeanFactory.getBean(nameToLookup);
				}
			}
            
            // 如果不僅僅是為了類型推斷,也就是代表我們要對進行實例化
            // 那麼就將bean標記為正在創建中,其實就是將這個beanName放入到alreadyCreated這個set集合中
			if (!typeCheckOnly) {
				markBeanAsCreated(beanName);
			}
			try {
                
				final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
                
                // 檢查合併后的bd是否是abstract,這個檢查現在已經沒有作用了,必定會通過
				checkMergedBeanDefinition(mbd, beanName, args);

				// @DependsOn註解標註的當前這個Bean所依賴的bean名稱的集合,就是說在創建當前這個Bean前,必須要先將其依賴的Bean先完成創建
				String[] dependsOn = mbd.getDependsOn();
				if (dependsOn != null) {
                    // 遍歷所有申明的依賴
					for (String dep : dependsOn) {
                        // 如果這個bean所依賴的bean又依賴了當前這個bean,出現了循環依賴,直接報錯
						if (isDependent(beanName, dep)) {
							throw new BeanCreationException(mbd.getResourceDescription(), beanName,
									"Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
						}
                        // 註冊bean跟其依賴的依賴關係,key為依賴,value為依賴所從屬的bean
						registerDependentBean(dep, beanName);
						try {
                            // 先創建其依賴的Bean
							getBean(dep);
						}
						catch (NoSuchBeanDefinitionException ex) {
							throw new BeanCreationException(mbd.getResourceDescription(), beanName,
									"'" + beanName + "' depends on missing bean '" + dep + "'", ex);
						}
					}
				}
				// 我們目前只分析單例的創建,單例看懂了,原型自然就懂了
				if (mbd.isSingleton()) {
                    // 這裏再次調用了getSingleton方法,這裏跟方法開頭調用的getSingleton的區別在於,這個方法多傳入了一個ObjectFactory類型的參數,這個ObjectFactory會返回一個Bean
					sharedInstance = getSingleton(beanName, () -> {
						try {
							return createBean(beanName, mbd, args);
						}
						catch (BeansException ex) {
							destroySingleton(beanName);
							throw ex;
						}
					});
					bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
				}
		// 省略原型跟域對象的相關代碼
		return (T) bean;
	}

配合註釋看這段代碼應該也不難吧,我們重點關注最後在調用的這段方法即可

4、getSingleton(beanName,ObjectFactory)

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
   Assert.notNull(beanName, "Bean name must not be null");
   synchronized (this.singletonObjects) {
       // 從單例池中獲取,這個地方肯定也獲取不到
      Object singletonObject = this.singletonObjects.get(beanName);
      if (singletonObject == null) {
          // 工廠已經在銷毀階段了,這個時候還在創建Bean的話,就直接拋出異常
         if (this.singletonsCurrentlyInDestruction) {
            throw new BeanCreationNotAllowedException(beanName,
                  "Singleton bean creation not allowed while singletons of this factory are in destruction " +
                  "(Do not request a bean from a BeanFactory in a destroy method implementation!)");
         }
         // 在單例創建前,記錄一下正在創建的單例的名稱,就是把beanName放入到singletonsCurrentlyInCreation這個set集合中去
         beforeSingletonCreation(beanName);
         boolean newSingleton = false;
         boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
         if (recordSuppressedExceptions) {
            this.suppressedExceptions = new LinkedHashSet<>();
         }
         try {
             // 這裏調用了singletonFactory的getObject方法,對應的實現就是在doGetBean中的那一段lambda表達式
            singletonObject = singletonFactory.getObject();
            newSingleton = true;
         }
        // 省略異常處理
         finally {
            if (recordSuppressedExceptions) {
               this.suppressedExceptions = null;
            }
             // 在單例完成創建后,將beanName從singletonsCurrentlyInCreation中移除
             // 標志著這個單例已經完成了創建
            afterSingletonCreation(beanName);
         }
         if (newSingleton) {
             // 添加到單例池中
            addSingleton(beanName, singletonObject);
         }
      }
      return singletonObject;
   }
}

分析完上面這段代碼,我們會發現,核心的創建Bean的邏輯就是在singletonFactory.getObject()這句代碼中,而其實現就是在doGetBean方法中的那一段lambda表達式,如下:

實際就是通過createBean這個方法創建了一個Bean然後返回,createBean又幹了什麼呢?

5、createBean

protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
      throws BeanCreationException {

   RootBeanDefinition mbdToUse = mbd;
    
    // 解析得到beanClass,為什麼需要解析呢?如果是從XML中解析出來的標籤屬性肯定是個字符串嘛
    // 所以這裏需要加載類,得到Class對象
   Class<?> resolvedClass = resolveBeanClass(mbd, beanName);
   if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) {
      mbdToUse = new RootBeanDefinition(mbd);
      mbdToUse.setBeanClass(resolvedClass);
   }
   // 對XML標籤中定義的lookUp屬性進行預處理,如果只能根據名字找到一個就標記為非重載的,這樣在後續就不需要去推斷到底是哪個方法了,對於@LookUp註解標註的方法是不需要在這裏處理的,AutowiredAnnotationBeanPostProcessor會處理這個註解
   try {
      mbdToUse.prepareMethodOverrides();
   }
   // 省略異常處理...
   try {
       // 在實例化對象前,會經過後置處理器處理
       // 這個後置處理器的提供了一個短路機制,就是可以提前結束整個Bean的生命周期,直接從這裏返回一個Bean
       // 不過我們一般不會這麼做,它的另外一個作用就是對AOP提供了支持,在這裡會將一些不需要被代理的Bean進行標記,就本文而言,你可以暫時理解它沒有起到任何作用
      Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
      if (bean != null) {
         return bean;
      }
   }
    // 省略異常處理...
   try {
       // doXXX方法,真正幹活的方法,doCreateBean,真正創建Bean的方法
      Object beanInstance = doCreateBean(beanName, mbdToUse, args);
      if (logger.isDebugEnabled()) {
         logger.debug("Finished creating instance of bean '" + beanName + "'");
      }
      return beanInstance;
   }
  // 省略異常處理...
}

6、doCreateBean

本文只探討對象是怎麼創建的,至於怎麼從一個對象變成了Bean,在後面的文章我們再討論,所以我們主要就關注下面這段代碼

// 這個方法真正創建了Bean,創建一個Bean會經過 創建對象 > 依賴注入 > 初始化 這三個過程,在這個過程中,BeanPostPorcessor會穿插執行,本文主要探討的是創建對象的過程,所以關於依賴注入及初始化我們暫時省略,在後續的文章中再繼續研究
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
      throws BeanCreationException {

   // Instantiate the bean.
   BeanWrapper instanceWrapper = null;
   if (mbd.isSingleton()) {
       // 這行代碼看起來就跟factoryBean相關,這是什麼意思呢?
       // 在下文我會通過例子介紹下,你可以暫時理解為,這個地方返回的就是個null
      instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
   }
   if (instanceWrapper == null) {
       // 這裏真正的創建了對象
      instanceWrapper = createBeanInstance(beanName, mbd, args);
   }
   // 省略依賴注入,初始化
}

這裏我先分析下this.factoryBeanInstanceCache.remove(beanName)這行代碼。這裏需要說一句,我寫的這個源碼分析的系列非常的細節,之所以選擇這樣一個個扣細節是因為我自己在閱讀源碼過程中經常會被這些問題阻塞,那麼藉著這些文章將自己踩過的坑分享出來可以減少作為讀者的你自己在閱讀源碼時的障礙,其次也能夠提升自己閱讀源碼的能力。如果你對這些細節不感興趣的話,可以直接跳過,能把握源碼的主線即可。言歸正傳,我們回到這行代碼this.factoryBeanInstanceCache.remove(beanName)。什麼時候factoryBeanInstanceCache這個集合中會有值呢?這裏我還是以示例代碼來說明這個問題,示例代碼如下:

public class Main {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
	}
}

// 沒有做什麼特殊的配置,就是掃描了需要的組件,測試時換成你自己的包名
@ComponentScan("com.dmz.source.instantiation")
@Configuration
public class Config {
}

// 這裏申明了一個FactoryBean,並且通過@DependsOn註解申明了這個FactoryBean的創建要在orderService之後,主要目的是為了在DmzFactoryBean創建前讓容器發生一次屬性注入
@Component
@DependsOn("orderService")
public class DmzFactoryBean implements FactoryBean<DmzService> {
	@Override
	public DmzService getObject() throws Exception {
		return new DmzService();
	}

	@Override
	public Class<?> getObjectType() {
		return DmzService.class;
	}
}

// 沒有通過註解的方式將它放到容器中,而是通過上面的DmzFactoryBean來管理對應的Bean
public class DmzService {
}

// OrderService中需要注入dmzService
@Component
public class OrderService {
	@Autowired
	DmzService dmzService;
}

在這段代碼中,因為我們明確的表示了DmzFactoryBean是依賴於orderService的,所以必定會先創建orderService再創建DmzFactoryBean,創建orderService的流程如下:

其中的屬性注入階段,我們需要細化,也可以畫圖如下:

為orderService進行屬性注入可以分為這麼幾步

  1. 找到需要注入的注入點,也就是orderService中的dmzService字段

  2. 根據字段的類型以及名稱去容器中查詢符合要求的Bean

  3. 當遍歷到一個FactroyBean時,為了確定其getObject方法返回的對象的類型需要創建這個FactroyBean(只會到對象級別),然後調用這個創建好的FactroyBean的getObjectType方法明確其類型並與注入點需要的類型比較,看是否是一個候選的Bean,在創建這個FactroyBean時就將其放入了factoryBeanInstanceCache中。

  4. 在確定了唯一的候選Bean之後,Spring就會對這個Bean進行創建,創建的過程又經過三個步驟

    • 創建對象
    • 屬性注入
    • 初始化

    在創建對象時,因為此時factoryBeanInstanceCache已經緩存了這個Bean對應的對象,所以直接通過this.factoryBeanInstanceCache.remove(beanName)這行代碼就返回了,避免了二次創建對象。

7、createBeanInstance

protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
   
   Class<?> beanClass = resolveBeanClass(mbd, beanName);
   // 省略異常
    
    // 通過bd中提供的instanceSupplier來獲取一個對象
    // 正常bd中都不會有這個instanceSupplier屬性,這裏也是Spring提供的一個擴展點,但實際上不常用
   Supplier<?> instanceSupplier = mbd.getInstanceSupplier();
   if (instanceSupplier != null) {
      return obtainFromSupplier(instanceSupplier, beanName);
   }

   // bd中提供了factoryMethodName屬性,那麼要使用工廠方法的方式來創建對象,工廠方法又會區分靜態工廠方法跟實例工廠方法
   if (mbd.getFactoryMethodName() != null) {
      return instantiateUsingFactoryMethod(beanName, mbd, args);
   }

   // 在原型模式下,如果已經創建過一次這個Bean了,那麼就不需要再次推斷構造函數了
   boolean resolved = false;  // 是否推斷過構造函數
   boolean autowireNecessary = false;  // 構造函數是否需要進行注入
   if (args == null) {
      synchronized (mbd.constructorArgumentLock) {
         if (mbd.resolvedConstructorOrFactoryMethod != null) {
            resolved = true;
            autowireNecessary = mbd.constructorArgumentsResolved;
         }
      }
   }
   if (resolved) {
      if (autowireNecessary) {
         return autowireConstructor(beanName, mbd, null, null);
      }
      else {
         return instantiateBean(beanName, mbd);
      }
   }

   // 推斷構造函數
   Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
   if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
         mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
      return autowireConstructor(beanName, mbd, ctors, args);
   }

   // 調用無參構造函數創建對象
   return instantiateBean(beanName, mbd);
}

上面這段代碼在Spring官網閱讀(一)容器及實例化 已經分析過了,但是當時我們沒有深究創建對象的細節,所以本文將詳細探討Spring中的這個對象到底是怎麼創建出來的,這也是本文的主題。

在Spring官網閱讀(一)容器及實例化 這篇文章中,我畫了下面這麼一張圖

從上圖中我們可以知道Spring在實例化對象的時候有這麼幾種方式

  1. 通過bd中的supplier屬性
  2. 通過bd中的factoryMethodName跟factoryBeanName
  3. 通過構造函數

我們接下來就一一分析其中的細節:

》通過bd中的supplier屬性實例化對象

在Spring官網閱讀(一)容器及實例化 文中介紹過這種方式,因為這種方式我們基本不會使用,並不重要,所以這裏就不再贅述,我這裏就直接給出一個使用示例,大家自行體會吧

public static void main(String[] args) {
		AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
    // 直接註冊一個Bean,並且指定它的supplier就是Service::new
		ac.registerBean("service", Service.class,Service::new,zhe'sh);
		ac.refresh();
		System.out.println(ac.getBean("service"));
}

》通過bd中的factoryMethodName跟factoryBeanName實例化對象

對應代碼如下:

protected BeanWrapper instantiateUsingFactoryMethod(
    String beanName, RootBeanDefinition mbd, @Nullable Object[] explicitArgs) {
    return new ConstructorResolver(this).instantiateUsingFactoryMethod(beanName, mbd, explicitArgs);
}

上面這段代碼主要幹了兩件事

  • 創建一個ConstructorResolver對象,從類名來看,它是一個構造器解析器
  • 調用了這個構造器解析器的instantiateUsingFactoryMethod方法,這個方法見名知意,使用FactoryMethod來完成實例化

基於此,我們解決一個問題,ConstructorResolver是什麼?

ConstructorResolver是什麼?

在要研究一個類前,我們最先應該從哪裡入手呢?很多沒有經驗的同學可能會悶頭看代碼,但是實際上最好的學習方式是先閱讀類上的javaDoc

ConstructorResolver上的javaDoc如下:

上面這段javaDoc翻譯過來就是這個類就是用來解析構造函數跟工廠方法的代理者,並且它是通過參數匹配的方式來進行推斷構造方法或者工廠方法

看到這裏不知道小夥伴們是否有疑問,就是明明這個類不僅負責推斷構造函數,還會負責推斷工廠方法,那麼為什麼類名會叫做ConstructorResolver呢?我們知道Spring的代碼在業界來說絕對是最規範的,沒有之一,這樣來說的話,這個類最合適的名稱應該是ConstructorAndFactoryMethodResolver才對,因為它不僅負責推斷了構造函數還負責推斷了工廠方法嘛!

這裏我需要說一下我自己的理解。對於一個Bean,它是通過構造函數完成實例化的,或者通過工廠方法實例化的,其實在這個Bean看來都沒有太大區別,這兩者都可以稱之為這個Bean的構造器,因為通過它們都能構造出一個Bean。所以Spring就把兩者統稱為構造器了,所以這個類名也就被稱為ConstructorResolver了。

Spring在很多地方體現了這種實現,例如在XML配置的情況下,不論我們是使用構造函數創建對象還是使用工廠方法創建對象,其參數的標籤都是使用constructor-arg。比如下面這個例子

<bean id="dmzServiceGetFromStaticMethod"
      factory-bean="factoryBean"
      factory-method="getObject">
    <constructor-arg type="java.lang.String" value="hello" name="s"/>
    <constructor-arg type="com.dmz.source.instantiation.service.DmzFactory" ref="factoryBean"/>
</bean>

<!--測試靜態工廠方法創建對象-->
<bean id="service"
      class="com.dmz.official.service.MyFactoryBean"
      factory-method="staticGet">
    <constructor-arg type="java.lang.String" value="hello"/>
</bean>

<bean id="dmzService" class="com.dmz.source.instantiation.service.DmzService">
    <constructor-arg name="s" value="hello"/>
</bean>

在對這個類有了大概的了解后,我們就需要來分析它的源碼,這裏我就不把它單獨拎出來分析了,我們藉著Spring的流程看看這個類幹了什麼事情

instantiateUsingFactoryMethod方法做了什麼?

核心目的:推斷出要使用的factoryMethod以及調用這個FactoryMethod要使用的參數,然後反射調用這個方法實例化出一個對象

這個方法的代碼太長了,所以我們將它拆分成為一段一段的來分析

方法參數分析

在分析上面的代碼之前,我們先來看看這個方法的參數都是什麼含義

方法上關於參數的介紹如圖所示

  • beanName:當前要實例化的Bean的名稱
  • mbd:當前要實例化的Bean對應的BeanDefinition
  • explicitArgs:這個參數在容器啟動階段我們可以認定它就是null,只有显示的調用了getBean方法,並且傳入了明確的參數,例如:getBean("dmzService","hello")這種情況下才會不為null,我們分析這個方法的時候就直接認定這個參數為null即可
第一段
public BeanWrapper instantiateUsingFactoryMethod(
    String beanName, RootBeanDefinition mbd, @Nullable Object[] explicitArgs) {
    // 第一段代碼:創建並初始話一個BeanWrapperImpl
    BeanWrapperImpl bw = new BeanWrapperImpl();
    this.beanFactory.initBeanWrapper(bw);
    // ......
}

BeanWrapperImpl是什麼呢?如果你看過我之前的文章:Spring官網閱讀(十四)Spring中的BeanWrapper及類型轉換,那麼你對這個類應該不會陌生,它就是對Bean進行了一層包裝,並且在創建Bean的時候以及進行屬性注入的時候能夠進行類型轉換。就算你沒看過之前的文章也沒關係,只要記住兩點

  • BeanWrapperImpl包裝了一個實例化好的對象
  • BeanWrapperImpl能夠對屬性進行類型轉換

其層級關係如下:

回到我們的源碼分析,我們先來看看new BeanWrapperImpl()做了什麼事情?

對應代碼如下:

// 第一步:調用空參構造
public BeanWrapperImpl() {
    // 調用另外一個構造函數,表示要註冊默認的屬性編輯器
    this(true);
}

// 這個構造函數表明是否要註冊默認編輯器,上面傳入的值為true,表示需要註冊
public BeanWrapperImpl(boolean registerDefaultEditors) {
    super(registerDefaultEditors);
}

// 調用到父類的構造函數,確定要使用默認的屬性編輯器
protected AbstractNestablePropertyAccessor(boolean registerDefaultEditors) {
    if (registerDefaultEditors) {
        registerDefaultEditors();
    }
    // 對typeConverterDelegate進行初始化
    this.typeConverterDelegate = new TypeConverterDelegate(this);
}

總的來說創建的過程非常簡單。第一,確定要註冊默認的屬性編輯器;第二,對typeConverterDelegate屬性進行初始化。

緊接着,我們看看在初始化這個BeanWrapper做了什麼?

// 初始化BeanWrapper,主要就是將容器中配置的conversionService賦值到當前這個BeanWrapper上
// 同時註冊定製的屬性編輯器
protected void initBeanWrapper(BeanWrapper bw) {
    bw.setConversionService(getConversionService());
    registerCustomEditors(bw);
}

還記得conversionService在什麼時候被放到容器中的嗎?就是在finishBeanFactoryInitialization的時候啦~!

conversionService屬性完成賦值后就開始註冊定製的屬性編輯器,代碼如下:

// 傳入的參數就是我們的BeanWrapper,它同時也是一個屬性編輯器註冊表
protected void registerCustomEditors(PropertyEditorRegistry registry) {
    PropertyEditorRegistrySupport registrySupport =
        (registry instanceof PropertyEditorRegistrySupport ? (PropertyEditorRegistrySupport) registry : null);
    if (registrySupport != null) {
        // 這個配置的作用就是在註冊默認的屬性編輯器時,可以增加對數組到字符串的轉換功能
        // 默認就是通過","來切割字符串轉換成數組,對應的屬性編輯器就是StringArrayPropertyEditor
        registrySupport.useConfigValueEditors();
    }
    // 將容器中的屬性編輯器註冊到當前的這個BeanWrapper
    if (!this.propertyEditorRegistrars.isEmpty()) {
        for (PropertyEditorRegistrar registrar : this.propertyEditorRegistrars) {
            registrar.registerCustomEditors(registry);
            // 省略異常處理~
        }
    }
    // 這裏我們沒有添加任何的自定義的屬性編輯器,所以肯定為空
    if (!this.customEditors.isEmpty()) {
        this.customEditors.forEach((requiredType, editorClass) ->
                                   registry.registerCustomEditor(requiredType, BeanUtils.instantiateClass(editorClass)));
    }
}
第二段
public BeanWrapper instantiateUsingFactoryMethod(
    String beanName, RootBeanDefinition mbd, @Nullable Object[] explicitArgs) {

    // 省略已經分析的第一段代碼,到這裏已經得到了一個具有類型轉換功能的BeanWrapper
	
    // 實例化這個Bean的工廠Bean
    Object factoryBean;
    // 工廠Bean的Class
    Class<?> factoryClass;
    // 靜態工廠方法或者是實例化工廠方法
    boolean isStatic;
	
    /*下面這段代碼就是為上面申明的這三個屬性賦值*/ 
    String factoryBeanName = mbd.getFactoryBeanName();
    // 如果創建這個Bean的工廠就是這個Bean本身的話,那麼直接拋出異常
    if (factoryBeanName != null) {
        if (factoryBeanName.equals(beanName)) {
            throw new BeanDefinitionStoreException(mbd.getResourceDescription(), beanName,
                                                   "factory-bean reference points back to the same bean definition");
        }
        // 得到創建這個Bean的工廠Bean
        factoryBean = this.beanFactory.getBean(factoryBeanName);
        if (mbd.isSingleton() && this.beanFactory.containsSingleton(beanName)) {
            throw new ImplicitlyAppearedSingletonException();
        }
        factoryClass = factoryBean.getClass();
        isStatic = false;
    }
    else {
        // factoryBeanName為null,說明是通過靜態工廠方法來實例化Bean的
        // 靜態工廠進行實例化Bean,beanClass屬性必須要是工廠的class,如果為空,直接報錯
        if (!mbd.hasBeanClass()) {
            throw new BeanDefinitionStoreException(mbd.getResourceDescription(), beanName,
                                                   "bean definition declares neither a bean class nor a factory-bean reference");
        }
        factoryBean = null;
        factoryClass = mbd.getBeanClass();
        isStatic = true;
    }
    // 省略後續代碼
}

小總結:

這段代碼很簡單,就是確認實例化當前這個Bean的工廠方法是靜態工廠還是實例工廠,如果是實例工廠,那麼找出對應的工廠Bean。

第三段
public BeanWrapper instantiateUsingFactoryMethod(
    String beanName, RootBeanDefinition mbd, @Nullable Object[] explicitArgs) {
    
    // 省略第一段,第二段代碼
    
    // 到這裏已經得到了一個BeanWrapper,明確了實例化當前這個Bean到底是靜態工廠還是實例工廠
    // 並且已經確定了工廠Bean
    
    // 最終確定的要用來創建對象的方法
    Method factoryMethodToUse = null;
    ArgumentsHolder argsHolderToUse = null;
    Object[] argsToUse = null;
	
    // 參數分析時已經說過,explicitArgs就是null
    if (explicitArgs != null) {
        argsToUse = explicitArgs;
    }
    else {
        // 下面這段代碼是什麼意思呢?
        // 在原型模式下,我們會多次創建一個Bean,所以Spring對參數以及所使用的方法做了緩存
        // 在第二次創建原型對象的時候會進入這段緩存的邏輯
        // 但是這裡有個問題,為什麼Spring對參數有兩個緩存呢?
        // 一:resolvedConstructorArguments
        // 二:preparedConstructorArguments
        // 這裏主要是因為,直接使用解析好的構造的參數,因為這樣會導致創建出來的所有Bean都引用同一個屬性
        Object[] argsToResolve = null;
        synchronized (mbd.constructorArgumentLock) {
            factoryMethodToUse = (Method) mbd.resolvedConstructorOrFactoryMethod;
            // 緩存已經解析過的工廠方法或者構造方法
            if (factoryMethodToUse != null && mbd.constructorArgumentsResolved) {
                // resolvedConstructorArguments跟preparedConstructorArguments都是對參數的緩存
                argsToUse = mbd.resolvedConstructorArguments;
                if (argsToUse == null) {
                    argsToResolve = mbd.preparedConstructorArguments;
                }
            }
        }
        if (argsToResolve != null) {
            // preparedConstructorArguments需要再次進行解析
            argsToUse = resolvePreparedArguments(beanName, mbd, bw, factoryMethodToUse, argsToResolve);
        }
    }
    // 省略後續代碼
}

小總結:

上面這段代碼應該沒什麼大問題,其核心思想就是從緩存中取已經解析出來的方法以及參數,這段代碼只會在原型模式下生效,因為單例的話對象只會創建一次嘛~!最大的問題在於,為什麼在對參數進行緩存的時候使用了兩個不同的集合,並且緩存后的參數還需要再次解析,這個問題我們暫且放着,不妨帶着這個問題往下看。

因為接下來要分析的代碼就比較複雜了,所以為了讓你徹底看到代碼的執行流程,下面我會使用示例+流程圖+文字的方式來分析源碼。

示例代碼如下(這個例子覆蓋接下來要分析的所有流程):

配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="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.xsd"
	   default-autowire="constructor"><!--這裏開啟自動注入,並且是通過構造函數進行自動注入-->

	<!--factoryObject 提供了創建對象的方法-->
	<bean id="factoryObject" class="com.dmz.spring.first.instantiation.service.FactoryObject"/>

	<!--提供一個用於測試自動注入的對象-->
	<bean class="com.dmz.spring.first.instantiation.service.OrderService" id="orderService"/>
	
    <!--主要測試這個對象的實例化過程-->
	<bean id="dmzService" factory-bean="factoryObject" factory-method="getDmz" scope="prototype">
		<constructor-arg name="name" value="dmz"/>
		<constructor-arg name="age" value="18"/>
		<constructor-arg name="birthDay" value="2020-05-23"/>
	</bean>
	
    <!--測試靜態方法實例化對象的過程-->
	<bean id="indexService" class="com.dmz.spring.first.instantiation.service.FactoryObject"
		  factory-method="staticGetIndex"/>
	
    <!--提供這個轉換器,用於轉換dmzService中的birthDay屬性,從字符串轉換成日期對象-->
	<bean class="org.springframework.context.support.ConversionServiceFactoryBean" id="conversionService">
		<property name="converters">
			<set>
				<bean class="com.dmz.spring.first.instantiation.service.ConverterStr2Date"/>
			</set>
		</property>
	</bean>
</beans>

測試代碼:

public class FactoryObject {

	public DmzService getDmz(String name, int age, Date birthDay, OrderService orderService) {
		System.out.println("getDmz with "+"name,age,birthDay and orderService");
		return new DmzService();
	}

	public DmzService getDmz(String name, int age, Date birthDay) {
		System.out.println("getDmz with "+"name,age,birthDay");
		return new DmzService();
	}

	public DmzService getDmz(String name, int age) {
		System.out.println("getDmz with "+"name,age");
		return new DmzService();
	}

	public DmzService getDmz() {
		System.out.println("getDmz with empty arg");
		return new DmzService();
	}

	public static IndexService staticGetIndex() {
		return new IndexService();
	}
}

public class DmzService {
}

public class IndexService {
}

public class OrderService {
}

public class ConverterStr2Date implements Converter<String, Date> {
	@Override
	public Date convert(String source) {
		try {
			return new SimpleDateFormat("yyyy-MM-dd").parse(source);
		} catch (ParseException e) {
			return null;
		}
	}
}

/**
 * @author 程序員DMZ
 * @Date Create in 23:14 2020/5/21
 * @Blog https://daimingzhi.blog.csdn.net/
 */
public class Main {
	public static void main(String[] args) {
		ClassPathXmlApplicationContext cc = new ClassPathXmlApplicationContext();
		cc.setConfigLocation("application.xml");
		cc.refresh();
		cc.getBean("dmzService");
        // 兩次調用,用於測試緩存的方法及參數
//		cc.getBean("dmzService");

	}
}

運行上面的代碼會發現,程序打印:

getDmz with name,age,birthDay and orderService

具體原因我相信你看了接下來的源碼分析自然就懂了

第四段
public BeanWrapper instantiateUsingFactoryMethod(
    String beanName, RootBeanDefinition mbd, @Nullable Object[] explicitArgs) {
//  第一段代碼:到這裏已經得到了一個BeanWrapper,並對這個BeanWrapper做了初始化
//  第二段代碼:明確了實例化當前這個Bean到底是靜態工廠還是實例工廠
//	第三段代碼:以及從緩存中取過了對應了方法以及參數

// 進入第四段代碼分析,執行到這段代碼說明是第一次實例化這個對象
if (factoryMethodToUse == null || argsToUse == null) {
			// 如果被cglib代理的話,獲取父類的class
			factoryClass = ClassUtils.getUserClass(factoryClass);
			// 獲取到工廠類中的所有方法,接下來要一步步從這些方法中篩選出來符合要求的方法
			Method[] rawCandidates = getCandidateMethods(factoryClass, mbd);
			List<Method> candidateList = new ArrayList<>();
    		// 第一步篩選:之前 在第二段代碼中已經推斷了方法是靜態或者非靜態的
    		// 所以這裏第一個要求就是要滿足靜態/非靜態這個條件
    		// 第二個要求就是必須符合bd中定義的factoryMethodName的名稱
    		// 其中第二個要求請注意,如果bd是一個configurationClassBeanDefinition,也就是說是通過掃描@Bean註解產生的,那麼在判斷時還會添加是否標註了@Bean註解
			for (Method candidate : rawCandidates) {
				if (Modifier.isStatic(candidate.getModifiers()) == isStatic && mbd.isFactoryMethod(candidate)) {
					candidateList.add(candidate);
				}
			}
    		// 將之前得到的方法集合轉換成數組
    		// 到這一步得到的其實就是某一個方法的所有重載方法
    	 	// 比如dmz(),dmz(String name),dmz(String name,int age)
			Method[] candidates = candidateList.toArray(new Method[0]);
    
    		// 排序,public跟參數多的優先級越高
			AutowireUtils.sortFactoryMethods(candidates);
			
    		// 用來保存從配置文件中解析出來的參數
			ConstructorArgumentValues resolvedValues = null;
            // 是否使用了自動注入,本段代碼中沒有使用到這個屬性,但是在後面用到了
			boolean autowiring = (mbd.getResolvedAutowireMode() == AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR);
			int minTypeDiffWeight = Integer.MAX_VALUE;
    		// 可能出現多個符合要求的方法,用這個集合保存,實際上如果這個集合有值,就會拋出異常了
			Set<Method> ambiguousFactoryMethods = null;

			int minNrOfArgs;
    		// 必定為null,不考慮了
			if (explicitArgs != null) {
				minNrOfArgs = explicitArgs.length;
			}
			else {
                // 就是說配置文件中指定了要使用的參數,那麼需要對其進行解析,解析后的值就存儲在resolvedValues這個集合中
				if (mbd.hasConstructorArgumentValues()) {
                    // 通過解析constructor-arg標籤,將參數封裝成了ConstructorArgumentValues
                    // ConstructorArgumentValues這個類在下文我們專門分析
					ConstructorArgumentValues cargs = mbd.getConstructorArgumentValues();
					resolvedValues = new ConstructorArgumentValues();
                    // 解析標籤中的屬性,類似進行類型轉換,後文進行詳細分析
					minNrOfArgs = resolveConstructorArguments(beanName, mbd, bw, cargs, resolvedValues);
				}
				else {
                    // 配置文件中沒有指定要使用的參數,所以執行方法的最小參數個數就是0
					minNrOfArgs = 0;
				}
			}
	// 省略後續代碼....
}

小總結:

因為在實例化對象前必定要先確定具體要使用的方法,所以這裏先做的第一件事就是確定要在哪個範圍內去推斷要使用的factoryMethod呢?

最大的範圍就是這個factoryClass的所有方法,也就是源碼中的rawCandidates

其次需要在rawCandidates中進一步做推斷,因為在前面第二段代碼的時候已經確定了是靜態方法還是非靜態方法,並且BeanDefinition也指定了factoryMethodName,那麼基於這兩個條件這裏就需要對rawCandidates進一步進行篩選,得到一個candidateList集合。

我們對示例的代碼進行調試會發現

確實如我們所料,rawCandidates是factoryClass中的所有方法,candidateList是所有getDmz的重載方法。

在確定了推斷factoryMethod的範圍后,那麼接下來要根據什麼去確定到底使用哪個方法呢?換個問題,怎麼區分這麼些重載的方法呢?肯定是根據方法參數嘛!

所以接下來要做的就是去解析要使用的參數了~

對於Spring而言,方法的參數會分為兩種

  1. 配置文件中指定的
  2. 自動注入模式下,需要去容器中查找的

在上面的代碼中,Spring就是將配置文件中指定的參數做了一次解析,對應方法就是resolveConstructorArguments

在查看這個方法的源碼前,我們先看看ConstructorArgumentValues這個類

public class ConstructorArgumentValues {
	// 通過下標方式指定的參數
	private final Map<Integer, ValueHolder> indexedArgumentValues = new LinkedHashMap<>();
	// 沒有指定下標
	private final List<ValueHolder> genericArgumentValues = new ArrayList<>();
	// 省略無關代碼.....
}

在前文的註釋中我們也說過了,它主要的作用就是封裝解析constructor-arg標籤得到的屬性,解析標籤對應的方法就是org.springframework.beans.factory.xml.BeanDefinitionParserDelegate#parseConstructorArgElement,這個方法我就不帶大家看了,有興趣的可以自行閱讀。

它主要有兩個屬性

  1. indexedArgumentValues
  2. genericArgumentValues

對應的就是我們兩種指定參數的方法,如下:

<bean id="dmzService" factory-bean="factoryObject" factory-method="getDmz" scope="prototype">
    <constructor-arg name="name" value="dmz"/>
    <constructor-arg name="age" value="18"/>
    <constructor-arg index="2"  value="2020-05-23"/>
    <!--		<constructor-arg name="birthDay" value="2020-05-23"/>-->
</bean>

其中的name跟age屬性會被解析為genericArgumentValues,而index=2會被解析為indexedArgumentValues

在對ConstructorArgumentValues有一定認知之後,我們再來看看resolveConstructorArguments的代碼:

// 方法目的:解析配置文件中指定的方法參數
// beanName:bean名稱
// mbd:beanName對應的beanDefinition
// bw:通過它進行類型轉換
// ConstructorArgumentValues cargs:解析標籤得到的屬性,還沒有經過解析(類型轉換)
// ConstructorArgumentValues resolvedValues:已經經過解析的參數
// 返回值:返回方法需要的最小參數個數
private int resolveConstructorArguments(String beanName, RootBeanDefinition mbd, BeanWrapper bw,
                                        ConstructorArgumentValues cargs, ConstructorArgumentValues resolvedValues) {
	
    // 是否有定製的類型轉換器,沒有的話直接使用BeanWrapper進行類型轉換
    TypeConverter customConverter = this.beanFactory.getCustomTypeConverter();
    TypeConverter converter = (customConverter != null ? customConverter : bw);
    
    // 構造一個BeanDefinitionValueResolver,專門用於解析constructor-arg中的value屬性,實際上還包括ref屬性,內嵌bean標籤等等
    BeanDefinitionValueResolver valueResolver =
        new BeanDefinitionValueResolver(this.beanFactory, beanName, mbd, converter);
	
    // minNrOfArgs 記錄執行方法要求的最小參數個數,一般情況下就是等於constructor-arg標籤指定的參數數量
    int minNrOfArgs = cargs.getArgumentCount();

    for (Map.Entry<Integer, ConstructorArgumentValues.ValueHolder> entry : cargs.getIndexedArgumentValues().entrySet()) {
        int index = entry.getKey();
        if (index < 0) {
            throw new BeanCreationException(mbd.getResourceDescription(), beanName,
                                            "Invalid constructor argument index: " + index);
        }
        // 這是啥意思呢?
        // 這個代碼我認為是有問題的,並且我給Spring官方已經提了一個issue,官方將會在5.2.7版本中修復
        // 暫且你先這樣理解
        // 假設A方法直接在配置文件中指定了index=3上要使用的參數,那麼這個時候A方法至少需要4個參數
        // 但是其餘的3個參數可能不是通過constructor-arg標籤指定的,而是直接自動注入進來的,那麼在配置文件中我們就只配置了index=3上的參數,也就是說 int minNrOfArgs = cargs.getArgumentCount()=1,這個時候 index=3,minNrOfArgs=1, 所以 minNrOfArgs = 3+1
        if (index > minNrOfArgs) {
            minNrOfArgs = index + 1;
        }
        ConstructorArgumentValues.ValueHolder valueHolder = entry.getValue();
        // 如果已經轉換過了,直接添加到resolvedValues集合中
        if (valueHolder.isConverted()) {
            resolvedValues.addIndexedArgumentValue(index, valueHolder);
        }
        else {
            // 解析value/ref/內嵌bean標籤等
            Object resolvedValue =
                valueResolver.resolveValueIfNecessary("constructor argument", valueHolder.getValue());
            // 將解析后的resolvedValue封裝成一個新的ValueHolder,並將其source設置為解析constructor-arg得到的那個ValueHolder,後期會用到這個屬性進行判斷
            ConstructorArgumentValues.ValueHolder resolvedValueHolder =
                new ConstructorArgumentValues.ValueHolder(resolvedValue, valueHolder.getType(), valueHolder.getName());
            resolvedValueHolder.setSource(valueHolder);
            resolvedValues.addIndexedArgumentValue(index, resolvedValueHolder);
        }
    }
   // 對getGenericArgumentValues進行解析,代碼基本一樣,不再贅述
    return minNrOfArgs;
}

可以看到,最終的解析邏輯就在resolveValueIfNecessary這個方法中,那麼這個方法又做了什麼呢?

// 這個方法的目的就是將解析constructor-arg標籤得到的value值進行一次解析
// 在解析標籤時ref屬性會被封裝為RuntimeBeanReference,那麼在這裏進行解析時就會去調用getBean
// 在解析value屬性會會被封裝為TypedStringValue,那麼這裡會嘗試去進行一個轉換
// 關於標籤的解析大家有興趣的話可以去看看org.springframework.beans.factory.xml.BeanDefinitionParserDelegate#parsePropertyValue
// 這裏不再贅述了
public Object resolveValueIfNecessary(Object argName, @Nullable Object value) {
	
    // 解析constructor-arg標籤中的ref屬性,實際就是調用了getBean
    if (value instanceof RuntimeBeanReference) {
        RuntimeBeanReference ref = (RuntimeBeanReference) value;
        return resolveReference(argName, ref);
    }
    
    // ......
    
       /**  
		 * <constructor-arg>
		 * 			<set value-type="java.lang.String">
		 * 				<value>1</value>
		 * 			</set>
		 * </constructor-arg>
		 * 通過上面set標籤中的value-type屬性對value進行類型轉換,
		 * 如果value-type屬性為空,那麼這裏不會進行類型轉換
		 */
   else if (value instanceof TypedStringValue) {
			TypedStringValue typedStringValue = (TypedStringValue) value;
			Object valueObject = evaluate(typedStringValue);
			try {
				Class<?> resolvedTargetType = resolveTargetType(typedStringValue);
				if (resolvedTargetType != null) {
					return this.typeConverter.convertIfNecessary(valueObject, resolvedTargetType);
				}
				else {
					return valueObject;
				}
			}
			catch (Throwable ex) {
				// Improve the message by showing the context.
				throw new BeanCreationException(
						this.beanDefinition.getResourceDescription(), this.beanName,
						"Error converting typed String value for " + argName, ex);
			}
		}
    // 省略後續代碼....
}

就我們上面的例子而言,經過resolveValueIfNecessary方法並不能產生實際的影響,因為在XML中我們沒有配置ref屬性或者value-type屬性。

畫圖如下:

第五段
public BeanWrapper instantiateUsingFactoryMethod(
    String beanName, RootBeanDefinition mbd, @Nullable Object[] explicitArgs) {
    //  第一段代碼:到這裏已經得到了一個BeanWrapper,並對這個BeanWrapper做了初始化
    //  第二段代碼:明確了實例化當前這個Bean到底是靜態工廠還是實例工廠
    //	第三段代碼:以及從緩存中取過了對應了方法以及參數
    //  第四段代碼:明確了方法需要的最小的參數數量並對配置文件中的標籤屬性進行了一次解析

    // 進入第五段代碼分析
    
    // 保存在創建方法參數數組過程中發生的異常,如果最終沒有找到合適的方法,那麼將這個異常信息封裝后拋出
    LinkedList<UnsatisfiedDependencyException> causes = null;
    
    // 開始遍歷所有在第四段代碼中查詢到的符合要求的方法
    for (Method candidate : candidates) {
        // 方法的參數類型
        Class<?>[] paramTypes = candidate.getParameterTypes();
        // 候選的方法的參數必須要大於在第四段這推斷出來的最小參數個數
        if (paramTypes.length >= minNrOfArgs) {
            ArgumentsHolder argsHolder;
            // 必定為null,不考慮
            if (explicitArgs != null) {
                // Explicit arguments given -> arguments length must match exactly.
                if (paramTypes.length != explicitArgs.length) {
                    continue;
                }
                argsHolder = new ArgumentsHolder(explicitArgs);
            }
            else {
                // Resolved constructor arguments: type conversion and/or autowiring necessary.
                try {
                    // 獲取參數的具體名稱
                    String[] paramNames = null;
                    ParameterNameDiscoverer pnd = this.beanFactory.getParameterNameDiscoverer();
                    if (pnd != null) {
                        paramNames = pnd.getParameterNames(candidate);
                    }
                    // 根據方法的參數名稱以及配置文件中配置的參數創建一個參數數組用於執行工廠方法
                    argsHolder = createArgumentArray(
                        beanName, mbd, resolvedValues, bw, paramTypes, paramNames, candidate, autowiring);
                }
                // 在創建參數數組的時候可能發生異常,這個時候的異常不能直接拋出,要確保所有的候選方法遍歷完成,只要有一個方法符合要求即可,但是如果遍歷完所有方法還是沒找到合適的構造器,那麼直接拋出這些異常
                catch (UnsatisfiedDependencyException ex) {
                    if (logger.isTraceEnabled()) {
                        logger.trace("Ignoring factory method [" + candidate + "] of bean '" + beanName + "': " + ex);
                    }
                    // Swallow and try next overloaded factory method.
                    if (causes == null) {
                        causes = new LinkedList<>();
                    }
                    causes.add(ex);
                    continue;
                }
                // 計算類型差異
                // 首先判斷bd中是寬鬆模式還是嚴格模式,目前看來只有@Bean標註的方法解析得到的Bean會使用嚴格模式來計算類型差異,其餘都是使用寬鬆模式
                // 嚴格模式下,
                int typeDiffWeight = (mbd.isLenientConstructorResolution() ?
                                      argsHolder.getTypeDifferenceWeight(paramTypes) : argsHolder.getAssignabilityWeight(paramTypes));
                // 選擇一個類型差異最小的方法
                if (typeDiffWeight < minTypeDiffWeight) {
                    factoryMethodToUse = candidate;
                    argsHolderToUse = argsHolder;
                    argsToUse = argsHolder.arguments;
                    minTypeDiffWeight = typeDiffWeight;
                    ambiguousFactoryMethods = null;
                }
   	// 省略後續代碼.......
    }

小總結:這段代碼的核心思想就是根據第四段代碼從配置文件中解析出來的參數構造方法執行所需要的實際參數數組。如果構建成功就代表這個方法可以用於實例化Bean,然後計算實際使用的參數跟方法上申明的參數的”差異值“,並在所有符合要求的方法中選擇一個差異值最小的方法

接下來,我們來分析方法實現的細節

  1. 構建方法使用的參數數組,也就是createArgumentArray方法,其源碼如下:
/* beanName:要實例化的Bean的名稱
 * mbd:對應Bean的BeanDefinition
 * resolvedValues:從配置文件中解析出來的並嘗試過類型轉換的參數
 * bw:在這裏主要就是用作類型轉換器
 * paramTypes:當前遍歷到的候選的方法的參數類型數組
 * paramNames:當前遍歷到的候選的方法的參數名稱
 * executable:當前遍歷到的候選的方法
 * autowiring:是否時自動注入
 */
private ArgumentsHolder createArgumentArray(
    String beanName, RootBeanDefinition mbd, @Nullable ConstructorArgumentValues resolvedValues,
			BeanWrapper bw, Class<?>[] paramTypes, @Nullable String[] paramNames, Executable executable,
			boolean autowiring) throws UnsatisfiedDependencyException {

		TypeConverter customConverter = this.beanFactory.getCustomTypeConverter();
		TypeConverter converter = (customConverter != null ? customConverter : bw);

		ArgumentsHolder args = new ArgumentsHolder(paramTypes.length);
		Set<ConstructorArgumentValues.ValueHolder> usedValueHolders = new HashSet<>(paramTypes.length);
		Set<String> autowiredBeanNames = new LinkedHashSet<>(4);
		// 遍歷候選方法的參數,跟據方法實際需要的類型到resolvedValues中去匹配
		for (int paramIndex = 0; paramIndex < paramTypes.length; paramIndex++) {
			Class<?> paramType = paramTypes[paramIndex];
			String paramName = (paramNames != null ? paramNames[paramIndex] : "");
			
			ConstructorArgumentValues.ValueHolder valueHolder = null;
			if (resolvedValues != null) {
                // 首先,根據方法參數的下標到resolvedValues中找對應的下標的屬性
                // 如果沒找到再根據方法的參數名/類型去resolvedValues查找
				valueHolder = resolvedValues.getArgumentValue(paramIndex, paramType, paramName, usedValueHolders);
				// 如果都沒找到
                // 1.是自動注入並且方法的參數長度正好跟配置中的參數數量相等
                // 2.不是自動注入
                // 那麼按照順序一次選取
				if (valueHolder == null && (!autowiring || paramTypes.length == resolvedValues.getArgumentCount())) {
					valueHolder = resolvedValues.getGenericArgumentValue(null, null, usedValueHolders);
				}
			}
			// 也就是說在配置的參數中找到了合適的值可以應用於這個方法上
			if (valueHolder != null) {
				// 防止同一個參數被應用了多次
				usedValueHolders.add(valueHolder);
				Object originalValue = valueHolder.getValue();
				Object convertedValue;
                // 已經進行過類型轉換就不會需要再次進行類型轉換
				if (valueHolder.isConverted()) {
					convertedValue = valueHolder.getConvertedValue();
					args.preparedArguments[paramIndex] = convertedValue;
				}
				else {
					// 嘗試將配置的值轉換成方法參數需要的類型
					MethodParameter methodParam = MethodParameter.forExecutable(executable, paramIndex);
					try {
                        // 進行類型轉換
						convertedValue = converter.convertIfNecessary(originalValue, paramType, methodParam);
					}
					catch (TypeMismatchException ex) {
						// 拋出UnsatisfiedDependencyException,在調用該方法處會被捕獲
					}
					Object sourceHolder = valueHolder.getSource();
                    // 只要是valueHolder存在,到這裏這個判斷必定成立
					if (sourceHolder instanceof ConstructorArgumentValues.ValueHolder) {
						Object sourceValue = ((ConstructorArgumentValues.ValueHolder) sourceHolder).getValue();
						args.resolveNecessary = true;
						args.preparedArguments[paramIndex] = sourceValue;
					}
				}

				args.arguments[paramIndex] = convertedValue;
				args.rawArguments[paramIndex] = originalValue;
			}
			else {
                // 方法執行需要參數,但是resolvedValues中沒有提供這個參數,也就是說這個參數是要自動注入到Bean中的
				MethodParameter methodParam = MethodParameter.forExecutable(executable, paramIndex);
				// 不是自動注入,直接拋出異常
				if (!autowiring) {
					// 拋出UnsatisfiedDependencyException,在調用該方法處會被捕獲
				}
				try {
                    // 自動注入的情況下,調用getBean獲取需要注入的Bean
					Object autowiredArgument =
							resolveAutowiredArgument(methodParam, beanName, autowiredBeanNames, converter);
                    // 把getBean返回的Bean封裝到本次方法執行時需要的參數數組中去
					args.rawArguments[paramIndex] = autowiredArgument;
					args.arguments[paramIndex] = autowiredArgument;
                    // 標誌這個參數是自動注入的
					args.preparedArguments[paramIndex] = new AutowiredArgumentMarker();
                    // 自動注入的情況下,在第二次調用時,需要重新處理,不能直接緩存
					args.resolveNecessary = true;
				}
				catch (BeansException ex) {
					// 拋出UnsatisfiedDependencyException,在調用該方法處會被捕獲
				}
			}
		}
		
    	// 註冊Bean之間的依賴關係
		for (String autowiredBeanName : autowiredBeanNames) {
			this.beanFactory.registerDependentBean(autowiredBeanName, beanName);
			if (logger.isDebugEnabled()) {
				logger.debug("Autowiring by type from bean name '" + beanName +
						"' via " + (executable instanceof Constructor ? "constructor" : "factory method") +
						" to bean named '" + autowiredBeanName + "'");
			}
		}

		return args;
	}

上面這段代碼說難也難,說簡單也簡單,如果要徹底看懂它到底幹了什麼還是很有難度的。簡單來說,它就是從第四段代碼解析出來的參數中查找當前的這個候選方法需要的參數。如果找到了,那麼嘗試對其進行類型轉換,將其轉換成符合方法要求的類型,如果沒有找到那麼還需要判斷當前方法的這個參數能不能進行自動注入,如果可以自動注入的話,那麼調用getBean得到需要的Bean,並將其注入到方法需要的參數中。

第六段
public BeanWrapper instantiateUsingFactoryMethod(
    String beanName, RootBeanDefinition mbd, @Nullable Object[] explicitArgs) {
    //  第一段代碼:到這裏已經得到了一個BeanWrapper,並對這個BeanWrapper做了初始化
    //  第二段代碼:明確了實例化當前這個Bean到底是靜態工廠還是實例工廠
    //	第三段代碼:以及從緩存中取過了對應了方法以及參數
    //  第四段代碼:明確了方法需要的最小的參數數量並對配置文件中的標籤屬性進行了一次解析
    //  第五段代碼:到這裏已經確定了可以使用來實例化Bean的方法是哪個
    
	// 省略拋出異常的代碼,就是在對推斷出來的方法做驗證
    // 1.推斷出來的方法不能為null
    // 2.推斷出來的方法返回值不能為void
    // 3.推斷出來的方法不能有多個
		
    // 對參數進行緩存
      if (explicitArgs == null && argsHolderToUse != null) {
         argsHolderToUse.storeCache(mbd, factoryMethodToUse);
      }
   }

   try {
      Object beanInstance;

      if (System.getSecurityManager() != null) {
         final Object fb = factoryBean;
         final Method factoryMethod = factoryMethodToUse;
         final Object[] args = argsToUse;
         beanInstance = AccessController.doPrivileged((PrivilegedAction<Object>) () ->
               beanFactory.getInstantiationStrategy().instantiate(mbd, beanName, beanFactory, fb, factoryMethod, args),
               beanFactory.getAccessControlContext());
      }
      else {
          // 反射調用對應方法進行實例化
          // 1.獲取InstantiationStrategy,主要就是SimpleInstantiationStrategy跟CglibSubclassingInstantiationStrategy,其中CglibSubclassingInstantiationStrategy主要是用來處理beanDefinition中的lookupMethod跟replaceMethod。通常來說我們使用的就是SimpleInstantiationStrateg
          // 2.SimpleInstantiationStrateg就是單純的通過反射調用方法
         beanInstance = this.beanFactory.getInstantiationStrategy().instantiate(
               mbd, beanName, this.beanFactory, factoryBean, factoryMethodToUse, argsToUse);
      }
		// beanWrapper在這裏對Bean進行了包裝
      bw.setBeanInstance(beanInstance);
      return bw;
   }
   catch (Throwable ex) {
      throw new BeanCreationException(mbd.getResourceDescription(), beanName,
            "Bean instantiation via factory method failed", ex);
   }
}

上面這段代碼的主要目的就是

  1. 緩存參數,原型可能多次創建同一個對象
  2. 反射調用推斷出來的factoryMethod

》通過構造函數實例化對象

如果上面你對使用factoryMethd進行實例化對象已經足夠了解的話,那麼下面的源碼分析基本沒有什麼很大區別,我們接着看看代碼。

首先,我們回到createBeanInstance方法中,

protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
    // 上面的代碼已經分析過了
    // 1.使用supplier來得到一個對象
    // 2.通過factotryMethod方法實例化一個對象
	
    // 看起來是不是有點熟悉,在使用factotryMethod創建對象時也有差不多這樣的一段代碼,看起來就是使用緩存好的方法直接創建一個對象
    boolean resolved = false;
    boolean autowireNecessary = false;
    
    // 不對這個參數進行討論,就認為一直為null
    if (args == null) {
        synchronized (mbd.constructorArgumentLock) {
            // bd中的resolvedConstructorOrFactoryMethod不為空,說明已經解析過構造方法了
            if (mbd.resolvedConstructorOrFactoryMethod != null) {
                // resolved標誌是否解析過構造方法
                resolved = true;
                autowireNecessary = mbd.constructorArgumentsResolved;
            }
        }
    }
    if (resolved) {
        // 構造函數已經解析過了,並且這個構造函數在調用時需要自動注入參數
        if (autowireNecessary) {
            // 此時部分解析好的參數已經存在了beanDefinition中,並且構造函數也在bd中
            // 那麼在這裏只會從緩存中去取構造函數以及參數然後反射調用
            return autowireConstructor(beanName, mbd, null, null);
        }
        else {
            // 這裏就是直接反射調用空參構造
            return instantiateBean(beanName, mbd);
        }
    }
	// 推斷出能夠使用的需要參數的構造函數
    Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
    // 在推斷出來的構造函數中選取一個合適的方法來進行Bean的實例化
    // ctors不為null:說明存在1個或多個@Autowired標註的方法
    // mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR:說明是自動注入
    // mbd.hasConstructorArgumentValues():配置文件中配置了構造函數要使用的參數
    // !ObjectUtils.isEmpty(args):外部傳入的參數,必定為null,不多考慮
    // 上面的條件只要滿足一個就會進入到autowireConstructor方法
    // 第一個條件滿足,那麼通過autowireConstructor在推斷出來的構造函數中再進一步選擇一個差異值最小的,參數最長的構造函數
    // 第二個條件滿足,說明沒有@Autowired標註的方法,但是需要進行自動注入,那麼通過autowireConstructor會去遍歷類中申明的所有構造函數,並查找一個差異值最小的,參數最長的構造函數
    // 第三個條件滿足,說明不是自動注入,那麼要通過配置中的參數去類中申明的所有構造函數中匹配
    // 第四個必定為null,不考慮
    if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
        mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
        return autowireConstructor(beanName, mbd, ctors, args);
    }
	
    // 反射調用空參構造
    return instantiateBean(beanName, mbd);
}

因為autowireConstructor方法的執行邏輯跟instantiateUsingFactoryMethod方法的執行邏輯基本一致,只是將Method對象換成了Constructor對象,所以對這個方法我不再做詳細的分析。

我們主要就看看determineConstructorsFromBeanPostProcessors這個方法吧,這個方法的主要目的就是推斷出候選的構造方法。

determineConstructorsFromBeanPostProcessors方法做了什麼?

// 實際調用的就是AutowiredAnnotationBeanPostProcessor中的determineCandidateConstructors方法
// 這個方法看起來很長,但實際確很簡單,就是通過@Autowired註解確定哪些構造方法可以作為候選方法,其實在使用factoryMethod來實例化對象的時候也有這種邏輯在其中,後續在總結的時候我們對比一下
public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, final String beanName)
			throws BeanCreationException {

		// 這裏做的事情很簡單,就是將@Lookup註解標註的方法封裝成LookupOverride添加到BeanDefinition中的methodOverrides屬性中,如果這個屬性不為空,在實例化對象的時候不能選用SimpleInstantiationStrateg,而要使用CglibSubclassingInstantiationStrategy,通過cglib代理給方法加一層攔截了邏輯
    	// 避免重複檢查
		if (!this.lookupMethodsChecked.contains(beanName)) {
			try {
				ReflectionUtils.doWithMethods(beanClass, method -> {
					Lookup lookup = method.getAnnotation(Lookup.class);
					if (lookup != null) {
						Assert.state(this.beanFactory != null, "No BeanFactory available");				// 將@Lookup註解標註的方法封裝成LookupOverride
						LookupOverride override = new LookupOverride(method, lookup.value());
						try {
                            // 添加到BeanDefinition中的methodOverrides屬性中
							RootBeanDefinition mbd = (RootBeanDefinition)
									this.beanFactory.getMergedBeanDefinition(beanName);
							mbd.getMethodOverrides().addOverride(override);
						}
						catch (NoSuchBeanDefinitionException ex) {
							throw new BeanCreationException(beanName,
									"Cannot apply @Lookup to beans without corresponding bean definition");
						}
					}
				});
			}
			catch (IllegalStateException ex) {
				throw new BeanCreationException(beanName, "Lookup method resolution failed", ex);
			}
			this.lookupMethodsChecked.add(beanName);
		}
		
    // 接下來要開始確定到底哪些構造函數能被作為候選者
		
    // 先嘗試從緩存中獲取
		Constructor<?>[] candidateConstructors = this.candidateConstructorsCache.get(beanClass);
		if (candidateConstructors == null) {
			// Fully synchronized resolution now...
			synchronized (this.candidateConstructorsCache) {
				candidateConstructors = this.candidateConstructorsCache.get(beanClass);、
                    // 緩存中無法獲取到,進入正式的推斷過程
				if (candidateConstructors == null) {
					Constructor<?>[] rawCandidates;
					try {
                        // 第一步:先查詢這個類所有的構造函數,包括私有的
						rawCandidates = beanClass.getDeclaredConstructors();
					}
					catch (Throwable ex) {
						// 省略異常信息
					}
					List<Constructor<?>> candidates = new ArrayList<>(rawCandidates.length);
                    // 保存添加了Autowired註解並且required屬性為true的構造方法
					Constructor<?> requiredConstructor = null;
                    // 空參構造
					Constructor<?> defaultConstructor = null;
                    // 看方法註釋上說明的,這裏除非是kotlin的類,否則必定為null,不做過多考慮,我們就將其當作null
					Constructor<?> primaryConstructor = BeanUtils.findPrimaryConstructor(beanClass);
					int nonSyntheticConstructors = 0;
                    // 對類中的所有構造方法進行遍歷
					for (Constructor<?> candidate : rawCandidates) {
                        // 非合成方法
						if (!candidate.isSynthetic()) {
							nonSyntheticConstructors++;
						}
						else if (primaryConstructor != null) {
							continue;
						}
                        // 查詢方法上是否有Autowired註解
						AnnotationAttributes ann = findAutowiredAnnotation(candidate);
						if (ann == null) {
                            // userClass != beanClass說明這個類進行了cglib代理
							Class<?> userClass = ClassUtils.getUserClass(beanClass);
							if (userClass != beanClass) {
								try {
                                    // 如果進行了cglib代理,那麼在父類上再次查找Autowired註解
									Constructor<?> superCtor =
											userClass.getDeclaredConstructor(candidate.getParameterTypes());
									ann = findAutowiredAnnotation(superCtor);
								}
								catch (NoSuchMethodException ex) {
									// Simply proceed, no equivalent superclass constructor found...
								}
							}
						}
                        // 說明當前的這個構造函數上有Autowired註解
						if (ann != null) {
							if (requiredConstructor != null) {
								// 省略異常拋出
							}
                            // 獲取Autowired註解中的required屬性
							boolean required = determineRequiredStatus(ann);
							if (required) {
                                // 類中存在多個@Autowired標註的方法,並且某個方法的@Autowired註解上被申明了required屬性要為true,那麼直接報錯
								if (!candidates.isEmpty()) {
								// 省略異常拋出
								}
								requiredConstructor = candidate;
							}
                            // 添加到集合中,這個集合存儲的都是被@Autowired註解標註的方法
							candidates.add(candidate);
						}
                        // 空參構造函數
						else if (candidate.getParameterCount() == 0) {
							defaultConstructor = candidate;
						}
					}
					if (!candidates.isEmpty()) {
						// 存在多個被@Autowired標註的方法
                        // 並且所有的required屬性被設置成了false (默認為true)
						if (requiredConstructor == null) {
                            // 存在空參構造函數,注意,空參構造函數可以不被@Autowired註解標註
							if (defaultConstructor != null) {
                                // 將空參構造函數也加入到候選的方法中去
								candidates.add(defaultConstructor);
							}
							// 省略日誌打印
						}
                        
						candidateConstructors = candidates.toArray(new Constructor<?>[0]);
					}
                    // 也就是說,類中只提供了一個構造函數,並且這個構造函數不是空參構造函數
					else if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0) {
						candidateConstructors = new Constructor<?>[] {rawCandidates[0]};
					}
                    // 省略中間兩個判斷,primaryConstructor必定為null,不考慮
					// .....
					}
					else {
                        // 說明無法推斷出來
						candidateConstructors = new Constructor<?>[0];
					}
					this.candidateConstructorsCache.put(beanClass, candidateConstructors);
				}
			}
		}
		return (candidateConstructors.length > 0 ? candidateConstructors : null);
	}

這裏我簡單總結下這個方法的作用

  1. 獲取到類中的所有構造函數

  2. 查找到被@Autowired註解標註的構造函數

    • 如果存在多個被@Autowired標註的構造函數,並且其required屬性沒有被設置為true,那麼返回這些被標註的函數的集合(空參構造即使沒有添加@Autowired也會被添加到集合中)
    • 如果存在多個被@Autowired標註的構造函數,並且其中一個的required屬性被設置成了true,那麼直接報錯
    • 如果只有一個構造函數被@Autowired標註,並且其required屬性被設置成了true,那麼直接返回這個構造函數
  3. 如果沒有被@Autowired標註標註的構造函數,但是類中有且只有一個構造函數,並且這個構造函數不是空參構造函數,那麼返回這個構造函數

  4. 上面的條件都不滿足,那麼determineCandidateConstructors這個方法就無法推斷出合適的構造函數了

可以看到,通過AutowiredAnnotationBeanPostProcessordetermineCandidateConstructors方法可以處理構造函數上的@Autowired註解。

但是,請注意,這個方法並不能決定到底使用哪個構造函數來創建對象(即使它只推斷出來一個,也不一定能夠使用),它只是通過@Autowired註解來確定構造函數的候選者,在構造函數都沒有添加@Autowired註解的情況下,這個方法推斷不出來任何方法。真正確定到底使用哪個構造函數是交由autowireConstructor方法來決定的。前文已經分析過了instantiateUsingFactoryMethod方法,autowireConstructor的邏輯基本跟它一致,所以這裏不再做詳細的分析。

factoryMethod跟構造函數的比較

整體邏輯比較

從上圖中可以看到,整體邏輯上它們並沒有什麼區別,只是查找的對象從factoryMethod換成了構造函數

執行細節比較

細節的差異主要體現在推斷方法上

  • 推斷factoryMethod
  • 推斷構造函數

它們之間的差異我已經在圖中標識出來了,主要就是兩點

  1. 通過構造函數實例化對象,多了一層處理,就是要處理構造函數上的@Autowired註解以及方法上的@LookUp註解(要決定選取哪一種實例化策略,SimpleInstantiationStrategy/CglibSubclassingInstantiationStrategy
  2. 在最終的選取也存在差異,對於facotyMehod而言,在寬鬆模式下(除ConfigurationClassBeanDefinition外,也就是掃描@Bean得到的BeanDefinition,都是寬鬆模式),會選取一個最精準的方法,在嚴格模式下,會選取一個參數最長的方法
  3. 對於構造函數而言,會必定會選取一個參數最長的方法

關於計算類型差異的補充內容

思考了很久,我還是決定再補充一些內容,就是關於上面兩幅圖的最後一步,對應的核心代碼如下:

int typeDiffWeight = (mbd.isLenientConstructorResolution() ?
                      argsHolder.getTypeDifferenceWeight(paramTypes) : argsHolder.getAssignabilityWeight(paramTypes));

if (typeDiffWeight < minTypeDiffWeight) {
    factoryMethodToUse = candidate;
    argsHolderToUse = argsHolder;
    argsToUse = argsHolder.arguments;
    minTypeDiffWeight = typeDiffWeight;
    ambiguousFactoryMethods = null;
}
  1. 判斷bd是嚴格模式還是寬鬆模式,上面說過很多次了,bd默認就是寬鬆模式,只要在ConfigurationClassBeanDefinition中使用嚴格模式,也就是掃描@Bean標註的方法註冊的bd(對應的代碼可以參考:org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForBeanMethod方法)

    我們再看看嚴格模式跟寬鬆模式在計算差異值時的區別

    • 寬鬆模式
    public int getTypeDifferenceWeight(Class<?>[] paramTypes) {
        // 計算實際使用的參數跟方法申明的參數的差異值
    			int typeDiffWeight = MethodInvoker.getTypeDifferenceWeight(paramTypes, this.arguments);
        // 計算沒有經過類型轉換的參數跟方法申明的參數的差異值
    			int rawTypeDiffWeight = MethodInvoker.getTypeDifferenceWeight(paramTypes, this.rawArguments) - 1024;
    			return (rawTypeDiffWeight < typeDiffWeight ? rawTypeDiffWeight : typeDiffWeight);
    		}
    
    public static int getTypeDifferenceWeight(Class<?>[] paramTypes, Object[] args) {
        int result = 0;
        for (int i = 0; i < paramTypes.length; i++) {
            // 在出現類型轉換時,下面這個判斷才會成立,也就是在比較rawArguments跟paramTypes的差異時才可能滿足這個條件
            if (!ClassUtils.isAssignableValue(paramTypes[i], args[i])) {
                return Integer.MAX_VALUE;
            }
            if (args[i] != null) {
                Class<?> paramType = paramTypes[i];
                Class<?> superClass = args[i].getClass().getSuperclass();
                while (superClass != null) {
                    // 如果我們傳入的值是方法上申明的參數的子類,那麼每多一層繼承關係,差異值加2
                    if (paramType.equals(superClass)) {
                        result = result + 2;
                        superClass = null;
                    }
                    else if (ClassUtils.isAssignable(paramType, superClass)) {
                        result = result + 2;
                        superClass = superClass.getSuperclass();
                    }
                    else {
                        superClass = null;
                    }
                }
                // 判斷方法的參數是不是一個接口,如果是,那麼差異值加1
                if (paramType.isInterface()) {
                    result = result + 1;
                }
            }
        }
        return result;
    }
    
    • 嚴格模式(主要應用於@Bean標註的方法對應的BeanDefinition)
    public int getAssignabilityWeight(Class<?>[] paramTypes) {
        // 嚴格模式下,只有三種返回值
        // 1.Integer.MAX_VALUE,經過類型轉換后還是不符合要求,返回最大的類型差異
        // 因為解析后的參數可能返回一個NullBean(創建對象的方法返回了null,Spring會將其包裝成一個NullBean),不過一般不會出現這種情況,所以我們可以當這種情況不存在
    			for (int i = 0; i < paramTypes.length; i++) {
    				if (!ClassUtils.isAssignableValue(paramTypes[i], this.arguments[i])) {
    					return Integer.MAX_VALUE;
    				}
    			}
        // 2.Integer.MAX_VALUE - 512,進行過了類型轉換才符合要求
    			for (int i = 0; i < paramTypes.length; i++) {
    				if (!ClassUtils.isAssignableValue(paramTypes[i], this.rawArguments[i])) {
    					return Integer.MAX_VALUE - 512;
    				}
    			}
        // 3.Integer.MAX_VALUE - 1024,沒有經過類型轉換就已經符合要求了,返回最小的類型差異
    			return Integer.MAX_VALUE - 1024;
    		}
    

    首先,不管是factoryMethod還是constructor,都是採用上面的兩個方法來計算類型差異,但是正常來說,只有factoryMethod會採用到嚴格模式(除非程序員手動干預,比如通過Bean工廠後置處理器修改了bd中的屬性,這樣通常來說沒有很大意義)

    所以我們分為三種情況討論

    1、factoryMethod+寬鬆模式

    這種情況下,會選取一個最精確的方法,同時方法的參數要盡量長

    測試代碼:

    public class FactoryObject {
    
    	public DmzService getDmz() {
    		System.out.println(0);
    		return new DmzService();
    	}
    
    	public DmzService getDmz(OrderService indexService) {
    		System.out.println(1);
    		return new DmzService();
    	}
    
    	public DmzService getDmz(OrderService orderService, IndexService indexService) {
    		System.out.println(2);
    		return new DmzService();
    	}
    
        public DmzService getDmz(OrderService orderService, IndexService indexService,IA ia) {
            System.out.println(3);
            return new DmzService();
        }
    }
    
    public class ServiceImpl implements IService {
    }
    
    public class IAImpl implements IA {
    }
    
    
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="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.xsd"
    default-autowire="constructor"><!--必須要開啟自動注入,並且是通過構造函數進行自動注入,否則選用無參構造-->
    
    	<!--factoryObject 提供了創建對象的方法-->
    	<bean id="factoryObject" class="com.dmz.spring.instantiation.service.FactoryObject"/>
    
    	<bean class="com.dmz.spring.instantiation.service.OrderService" id="orderService"/>
    
    	<bean id="dmzService" factory-bean="factoryObject" factory-method="getDmz" />
    
    	<bean class="com.dmz.spring.instantiation.service.ServiceImpl" id="iService"/>
    
    	<bean class="com.dmz.spring.instantiation.service.IndexService" id="indexService"/>
    
    </beans>
    
    /**
     * @author 程序員DMZ
     * @Date Create in 23:59 2020/6/1
     * @Blog https://daimingzhi.blog.csdn.net/
     */
    public class XMLMain {
    	public static void main(String[] args) {
    		ClassPathXmlApplicationContext cc =
    				new ClassPathXmlApplicationContext("application.xml");
    	}
    }
    

    運行程序發現,選用了第三個(getDmz(OrderService orderService, IndexService indexService))構造方法。雖然最後一個方法的參數更長,但是因為其方法申明的參數上存在接口,所以它的差異值會大於第三個方法,因為不會被選用

    2、factoryMethod+嚴格模式

    這種情況下,會選取一個參數盡量長的方法

    測試代碼:

    /**
     * @author 程序員DMZ
     * @Date Create in 6:28 2020/6/1
     * @Blog https://daimingzhi.blog.csdn.net/
     */
    @ComponentScan("com.dmz.spring.instantiation")
    @Configuration
    public class Config {
    
    	@Bean
    	public DmzService dmzService() {
    		System.out.println(0);
    		return new DmzService();
    	}
    	@Bean
    	public DmzService dmzService(OrderService indexService) {
    		System.out.println(1);
    		return new DmzService();
    	}
    	@Bean
    	public DmzService dmzService(OrderService orderService, IndexService indexService) {
    		System.out.println(2);
    		return new DmzService();
    	}
    	@Bean
    	public DmzService dmzService(OrderService orderService, IndexService indexService, IA ia) {
    		System.out.println("config " +3);
    		return new DmzService();
    	}
    
    	@Bean
    	public DmzService dmzService(OrderService orderService, IndexService indexService, IA ia, IService iService) {
    		System.out.println("config " +4);
    		return new DmzService();
    	}
    }
    
    /**
     * @author 程序員DMZ
     * @Date Create in 6:29 2020/6/1
     * @Blog https://daimingzhi.blog.csdn.net/
     */
    public class Main {
    	public static void main(String[] args) {
    		AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
    		ac.register(Config.class);
    		ac.refresh();
    	}
    }
    

    運行程序,發現選用了最後一個構造函數,這是因為在遍歷候選方法時,會先遍歷參數最長的,而在計算類型差異時,因為嚴格模式下,上面所有方法的差異值都是一樣的,都會返回Integer.MAX_VALUE - 1024。實際上,在不進行手動干預的情況下,都會返滬這個值。

    3、構造函數+寬鬆模式

    這種情況下,也會選取一個參數盡量長的方法

    之所以會這樣,主要是因為在autowireConstructor方法中進行了一次短路判斷,如下所示:

    在上圖中,如果已經找到了合適的方法,那麼直接就不會再找了,而在遍歷的時候是從參數最長的方法開始遍歷的,測試代碼如下:

    @Component
    public class DmzService {
    	
        // 沒有添加@Autowired註解,也會被當作候選方法
    	public DmzService(){
    		System.out.println(0);
    	}
    
    	@Autowired(required = false)
    	public DmzService(OrderService orderService) {
    		System.out.println(1);
    	}
    
    	@Autowired(required = false)
    	public DmzService(OrderService orderService, IService iService) {
    		System.out.println(2);
    	}
    
    	@Autowired(required = false)
    	public DmzService(OrderService orderService, IndexService indexService, IService iService,IA ia) {
    		System.out.println("DmzService "+3);
    	}
    }
    
    /**
     * @author 程序員DMZ
     * @Date Create in 6:29 2020/6/1
     * @Blog https://daimingzhi.blog.csdn.net/
     */
    public class Main {
    	public static void main(String[] args) {
    		AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
    		ac.register(Config.class);
    		ac.refresh();
    	}
    }
    

    這篇文章就到這裏啦~~!

    文章很長,希望你耐心看完,碼字不易,如果有幫助到你的話點個贊吧~!

掃描下方二維碼,關注我的公眾號,更多精彩文章在等您!~~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

日本防疫、核安難兩全 民眾與科學家齊倡停止核電運轉

文:宋瑞文(媽媽監督核電廠聯盟特約撰述)

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

RocketMQ系列(五)廣播與延遲消息

今天要給大家介紹RocketMQ中的兩個功能,一個是“廣播”,這個功能是比較基礎的,幾乎所有的mq產品都是支持這個功能的;另外一個是“延遲消費”,這個應該算是RocketMQ的特色功能之一了吧。接下來,我們就分別看一下這兩個功能。

廣播

廣播是把消息發送給訂閱了這個主題的所有消費者。這個定義很清楚,但是這裏邊的知識點你都掌握了嗎?咱們接着說“廣播”的機會,把消費者這端的內容好好和大家說說。

  • 首先,消費者端的概念中,最大的應該是消費者組,一個消費者組中可以有多個消費者,這些消費者必須訂閱同一個Topic。
  • 那麼什麼算是一個消費者呢?我們在寫消費端程序時,看到了setConsumeThreadMax這個方法,設置消費者的線程數,難道一個線程就是一個消費者?錯!這裏的一個消費者是一個進程,你可以理解為ip+端口。如果在同一個應用中,你實例化了兩個消費者,這兩個消費者配置了相同的消費者組名稱,那麼應用程序啟動時會報錯的,這裏不給大家演示了,感興趣的小夥伴私下里試一下吧。
  • 同一個消息,可以被不同的消費者組同時消費。假設,我有兩個消費者組cg-1和cg-2,這兩個消費者組訂閱了同一個Topic,那麼這個Topic的消息會被cg-1和cg-2同時消費。那這是不是廣播呢?錯!當然不是廣播,廣播是同一個消費者組中的多個消費者都消費這個消息。如果配置的不是廣播,像前幾個章節中的那樣,一個消息只能被一個消費者組消費一次。

好了,說了這麼多,我們實驗一下吧,先把消費者配置成廣播,如下:

@Bean(name = "broadcast", initMethod = "start",destroyMethod = "shutdown")
public DefaultMQPushConsumer broadcast() throws MQClientException {
    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("broadcast");
    consumer.setNamesrvAddr("192.168.73.130:9876;192.168.73.131:9876;192.168.73.132:9876;");
    consumer.subscribe("cluster-topic","*");
    consumer.setMessageModel(MessageModel.BROADCASTING);
    consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
        for (MessageExt msg : msgs) {
            System.out.println(new String(msg.getBody()));
        }
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    });
    return consumer;
}

  • 其中,NameServer,訂閱的Topic都沒有變化。
  • 注意其中consumer.setMessageModel(MessageModel.BROADCASTING);這段代碼,設置消費者為廣播。咱們可以看一下,MessageModel枚舉中只有兩個值,BROADCASTINGCLUSTERING,默認為CLUSTERING

因為要測試廣播,所以我們要啟動多個消費者,還記得什麼是消費者嗎?對了,一個ip+端口算是一個消費者,在這裏我們啟動兩個應用,端口分別是8080和8081。發送端的程序不變,如下:

@Test
public void producerTest() throws Exception {

    for (int i = 0;i<5;i++) {
        MessageExt message = new MessageExt();
        message.setTopic("cluster-topic");
        message.setKeys("key-"+i);
        message.setBody(("this is simpleMQ,my NO is "+i+"---"+new Date()).getBytes());
        SendResult sendResult = defaultMQProducer.send(message);
        System.out.println("i=" + i);
        System.out.println("BrokerName:" + sendResult.getMessageQueue().getBrokerName());
    }
}

我們執行一下發送端的程序,日誌如下:

i=0
BrokerName:broker-a
i=1
BrokerName:broker-a
i=2
BrokerName:broker-b
i=3
BrokerName:broker-b
i=4
BrokerName:broker-b

再來看看8080端口的應用後台打印出來的日誌:

消費了5個消息,再看看8081的後台打印的日誌,

也消費了5個。兩個消費者同時消費了消息,這就是廣播。有的小夥伴可能會有疑問了,如果不設置廣播,會怎麼樣呢?私下里實驗一下吧,上面的程序中,只要把設置廣播的那段代碼註釋掉就可以了。運行的結果當然是只有一個消費者可以消費消息。

延遲消息

延遲消息是指消費者過了一個指定的時間后,才去消費這個消息。大家想象一個電商中場景,一個訂單超過30分鐘未支付,將自動取消。這個功能怎麼實現呢?一般情況下,都是寫一個定時任務,一分鐘掃描一下超過30分鐘未支付的訂單,如果有則被取消。這種方式由於每分鐘查詢一下訂單,一是時間不精確,二是查庫效率比較低。這個場景使用RocketMQ的延遲消息最合適不過了,我們看看怎麼發送延遲消息吧,發送端代碼如下:

@Test
public void producerTest() throws Exception {

    for (int i = 0;i<1;i++) {
        MessageExt message = new MessageExt();
        message.setTopic("cluster-topic");
        message.setKeys("key-"+i);
        message.setBody(("this is simpleMQ,my NO is "+i+"---"+new Date()).getBytes());
        message.setDelayTimeLevel(2);
        SendResult sendResult = defaultMQProducer.send(message);
        System.out.println("i=" + i);
        System.out.println("BrokerName:" + sendResult.getMessageQueue().getBrokerName());
    }
}
  • 我們只是增加了一句message.setDelayTimeLevel(2);
  • 為了方便,這次我們只發送一個消息。

setDelayTimeLevel是什麼意思,設置的是2,難道是2s后消費嗎?怎麼參數也沒有時間單位呢?如果我要自定義延遲時間怎麼辦?我相信很多小夥伴都有這樣的疑問,我也是帶着這樣的疑問查了很多資料,最後在RocketMQ的Github官網上看到了說明,

  • 在RocketMQ的源碼中,有一個MessageStoreConfig類,這個類中定義了延遲的時間,我們看一下,
// org/apache/rocketmq/store/config/MessageStoreConfig.java
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
  • 我們在程序中設置的是2,那麼這個消息將在5s以後被消費。
  • 目前RocketMQ還不支持自定義延遲時間,延遲時間只能從上面的時間中選。如果你非要定義一個時間怎麼辦呢?RocketMQ是開源的,下載代碼,把上面的時間改一下,再打包部署,就OK了。

再看看消費端的代碼,

@Bean(name = "broadcast", initMethod = "start",destroyMethod = "shutdown")
public DefaultMQPushConsumer broadcast() throws MQClientException {
    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("broadcast");
    consumer.setNamesrvAddr("192.168.73.130:9876;192.168.73.131:9876;192.168.73.132:9876;");
    consumer.subscribe("cluster-topic","*");
    consumer.setMessageModel(MessageModel.BROADCASTING);
    consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
        for (MessageExt msg : msgs) {
            Date now = new Date();
            System.out.println("消費時間:"+now);

            Date msgTime = new Date();
            msgTime.setTime(msg.getBornTimestamp());
            System.out.println("消息生成時間:"+msgTime);

            System.out.println(new String(msg.getBody()));
        }
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    });
    return consumer;
}
  • 我們還是使用廣播的模式,沒有變。
  • 打印出了當前的時間,這個時間就是消費的時間。
  • 通過msg.getBornTimestamp()方法,獲得了消息的生成時間,也打印出來,看看是不是延遲5s。

啟動兩個消費者8080和8081,發送消息,再看看消費者的後台日誌,

消費時間:Thu Jun 11 14:45:53 CST 2020
消息生成時間:Thu Jun 11 14:45:48 CST 2020
this is simpleMQ,my NO is 0---Thu Jun 11 14:45:47 CST 2020

我們看到消費時間比生成時間晚5s,符合我們的預期。這個功能還是比較實用的,如果能夠自定義延遲時間就更好了。

總結

RocketMQ的這兩個知識點還是比較簡單的,大家要分清楚什麼是消費者組,什麼是消費者,什麼是消費者線程。另外就是延遲消息是不支持自定義的,大家可以在Github上看一下源碼。好了~今天就到這裏了。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

加拿大最後完整冰棚崩塌 面積縮 43% 、分裂出冰山面積比曼哈頓大

摘錄自2020年8月11日立場新聞報導

今年7月的最後兩日,加拿大北極群島中最北的埃爾斯米爾島(Ellesmere Island)米爾恩冰棚(Milne Ice Shelf)崩塌,使加拿大最後一個完整的冰棚面積減少了 43%,該冰塊其後漂入北冰洋,進一步分成兩大塊,並被歐洲太空總署哥白尼計劃哨兵衛星所拍攝到。

塌下的冰棚約有 80 平方公里,比 60 平方公里的曼哈頓更大。根據加拿大極冰局,高於正常的空氣溫度、離岸風和冰棚前的開揚水域,都是造成冰棚破裂的原因。由於冰棚破裂,北極最後一個已知的棚外湖 (epishelf lake) 可能消失。

冰棚可像活塞一樣,減緩與阻擋冰蓋與融冰水流入海洋的速度,有助於限制全球海水水位上升。另外,冰棚崩塌,造成的冰山可能會危害當地的航運業。

棚外湖是困於浮在海水上冰棚的淡水水體。當米爾恩冰棚崩塌時,可能將棚外湖以及其所含的淡水送進北冰洋,影響其鹹度,不過專家不確定其影響程度,情況將取決於米爾恩冰棚剩餘部分的完整性。

氣候變遷
國際新聞
加拿大
北極
冰棚
冰山融化

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

磚頭也能變電池?儲能研究揭最新奈米科技 關鍵因素在「多孔」

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

Asp.Net Core入門之自定義服務註冊

談到服務註冊,首先我們先了解一下服務註冊時使用的三種方式,也代表了不同的服務生命周期

1  AddTransient
 
2  AddScoped
 
3  AddSingleton
AddSingleton生命周期最長,其生命周期範圍描述為:從應用程序啟動到應用程序結束。在第一次請求時會創建一個實例,之後的每次請求都會使用同一個實例。
AddTransient生命周期最短,在服務請求時會創建一個實例,服務請求結束生命周期即結束,之後的每一次請求都會創建不同的實例。
AddSingleton生命周期介於上述兩者之間,這裏用客戶端請求會話的概念來描述比較清晰一點,它也是在服務請求時創建實例,但是在同一個會話周期內,之後的每次請求都會使用同一個實例,直至會話結束才會創建新的實例。
 

ASP.Net Core框架支持我們以如下方式註冊我們自己的服務。

services.AddScoped<ITest, Test>();

其中第一個泛型類型(如:ITest)表示將要從容器中請求的類型(通常是一個接口)。第二個泛型類型(如:Test)表示將由容器實例化並且用於完成這些請求的具體實現類。

具體我們一起看下面的例子:

首先,我們創建一個需要實現查詢功能的服務接口ITest

   public interface ITest
    {
        Task<string> Get();
    }

然後,我們創建功能類Test實現這個接口

 1     public class Test : ITest
 2     {
 3         private readonly ILogger logger;
 4         public Test(ILogger<Test> _logger)
 5         {
 6             logger = _logger;
 7         }
 8         public Task<string> Get()
 9         {
10             logger.LogInformation("自定義服務查詢");
11             return Task.FromResult("Hello World");
12         }
13     }

最後,我們需要我們自己的服務註冊到容器中。

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<ITest, Test>();
        }

以上我們便簡單完成了自定義服務的註冊。

隨後我這裏創建了一個Controller用以使用該服務。

 1     [Route("api/[controller]")]
 2     [ApiController]
 3     public class ValuesController : ControllerBase
 4     {
 5         //聲明服務
 6         private readonly ITest service;
 7 
 8         /// <summary>
 9         /// 通過構造函數的方式注入自定義服務類
10         /// </summary>
11         /// <param name="_service"></param>
12         public ValuesController(ITest _service)
13         {
14             service = _service;
15         }
16 
17         /// <summary>
18         /// 調用服務中實現的Get方法
19         /// </summary>
20         /// <returns></returns>
21         [HttpGet]
22         public Task<string> Get()
23         {
24             return service.Get();
25         }
26      }

ASP.Net Core框架默認支持我們以構造函數的方式注入我們的服務以使用。

我想寫到這裏,大家也會有疑問,如果我們有很多service,這樣一個個註冊寫起來代碼很低效,這裏我們簡單給大家介紹一種批量註冊的方式:

這裏我們創建了一個批量註冊服務派生類:

 1 public static class ServiceExtensions
 2     {
 3         /// <summary>
 4         /// 批量註冊程序集下的服務類
 5         /// </summary>
 6         /// <param name="services"></param>
 7         public static IServiceCollection AddBatchServices(this IServiceCollection services)
 8         {
 9             //根據指定程序集名稱獲取待註冊服務
10             var batchServices = GetConfigureClass("WebApiApplication");
11             foreach (var type in batchServices)
12             {
13                 type.Value.ToList().ForEach(i =>
14                 {
15                     //註冊服務類
16                     services.AddScoped(i, type.Key);
17                 });
18             }
19             return services;
20         }
21 
22         /// <summary>
23         /// 根據程序集名稱獲取自定義服務
24         /// </summary>
25         /// <param name="assembly"></param>
26         /// <returns></returns>
27         public static Dictionary<Type, Type[]> GetConfigureClass(string assembly)
28         {
29             Dictionary<Type, Type[]> dic = new Dictionary<Type, Type[]>();
30             if (!string.IsNullOrEmpty(assembly))
31             {
32                 //獲取程序集對應的類型
33                 Assembly dll = Assembly.LoadFrom(assembly);
34                 List<Type> lstType = dll.GetTypes().ToList();
35                 lstType.ForEach(x =>
36                 {
37                     //篩選滿足條件的服務類
38                     if (x.IsClass && x.GetInterfaces().Length > 0)
39                     {
40                         dic.Add(x, x.GetInterfaces());
41                     }
42                 });
43             }
44             return dic;
45         }
46    }

然後我們ConfigureServices方法中註冊:

        public void ConfigureServices(IServiceCollection services)
        {
            //批量註冊
            services.AddBatchServices();
        }

對於批量註冊,ASP.Net Core允許我們更換默認的IOC容器,感興趣的同學可以試試AutoFac容器支持的程序集掃描式註冊。

註冊我們自己的服務,往往在項目開發過程中是必要的,希望以上簡單的分享能給需要的小夥伴們帶來一點收貨。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

野火包圍 舊金山灣區空污嚴重危害居民健康

摘錄自2020年8月25日中央社報導

舊金山灣區四周被野火包圍,有害的煙塵讓許多民眾呼吸困難、眼睛發炎,原本有呼吸系統宿疾者病情加劇。舊金山灣區空氣品質管理區(Bay Area Air Quality Management District)指出,野火將有害的煙塵送入天空,讓今天的空氣品質盤旋在不健康的指數。

舊金山多個城市的細懸浮微粒(PM2.5)都達到對健康有威脅的程度,並讓許多民眾難以呼吸。舊金山紀事報網站今(25日)指出,許多居住在火災區附近的醫生通報,有呼吸系統宿疾的患者在上週末病情加劇,也有醫生認為未來幾週內都將陸續出現更多呼吸系統疾病相關的病人。

公衛專家警告,惡化的空氣品質伴隨著武漢肺炎(COVID-19)疫情,會讓原本就有氣喘與肺部疾病的患者更不舒服,同時可能讓染疫的民眾更虛弱。

污染治理
國際新聞
美國
加州
舊金山
空氣污染
空氣品質
火災

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

國產SUV新貴GS8大熱 那麼更便宜的GS7會怎樣呢?

GS7的側面造型和他大哥在C柱之前相似度較高,但是因為GS7的軸距較短,所以GS7看起來顯得緊湊很多。不過GS7的側窗並沒有沿襲GS8隱藏式的D柱設計,而是被銀色的鍍鉻裝飾條完整的包圍起來。對於這種設計,小編覺得GS7前臉都和大哥相似了,如果這部分再在用相同的設計,那麼設計師肯定會被吐槽偷懶的,其實這樣的設計也好,畢竟國人也很喜歡鍍鉻裝飾。

GS8無疑是2016年一款極其重要的車型,但是GS8全係為七座車型,有些家庭用不上那麼多的座位,買來之後便成了擺設。這一點讓喜歡GS8的人有點拿不定主意了。

所以大家都在議論着,要是GS8有個五座車型就好了。其實這點不用擔心的,因為傳祺在推出GS8的時候就已經有了一些關於GS7的傳聞,只不過,這次的GS7,終於要以真面目示眾了。

說到GS7那麼必須要提到北美車展,北美車展是世界五大車展之一,同時也是北美洲規模最大的國際車展,此車展每年一月份在美國底特律舉辦。因為是舉辦的日期比較特殊(新年的第一個大規模車展),所以北美車展具有很強的市場前瞻性,當然,我們很期待的傳祺GS7,也在此時亮相了。

廣汽傳祺GS7在北美車展全球首發,看點十足,當然不能錯過。GS7和GS8關係非常密切,你可以看作是短軸距版的GS8,GS7定位中型SUV,全係為五座車型。

GS7的車身長度和軸距要比GS8短80mm,但是寬度和高度與GS8一致。要說外觀,尤其是車頭的部分,看了之後你的第一反應肯定會是這個車頭真是很霸氣,但是隨後你可能會想,怎麼感覺這麼面熟呢?沒錯,因為GS7的車頭部分的設計基本和GS8一致,凌雲翼式的造型是很大氣,因為見過了GS8,所以當你看到GS7前臉的時候就不會覺得那麼驚艷了。這就是所謂的家族式設計,雖然依舊好看,但是不會讓消費者感到會有大的新鮮感。

小編最喜歡大燈組的設計,看起來感覺很高級,但是又不會顯得很臃腫。至於大家期待的LED大燈應該不會全系標配了(可以參考GS8)。

GS7的側面造型和他大哥在C柱之前相似度較高,但是因為GS7的軸距較短,所以GS7看起來顯得緊湊很多。

不過GS7的側窗並沒有沿襲GS8隱藏式的D柱設計,而是被銀色的鍍鉻裝飾條完整的包圍起來。對於這種設計,小編覺得GS7前臉都和大哥相似了,如果這部分再在用相同的設計,那麼設計師肯定會被吐槽偷懶的,其實這樣的設計也好,畢竟國人也很喜歡鍍鉻裝飾。

剛說過上面的不同點,這下子又被打臉了。因為尾部造型和大哥幾乎完全一致,肉眼很難看出有什麼太大的差別,畢竟,這兩個車子關係很親密么!

內飾還是那一句話,參考GS8,GS7的內飾造型大致和GS8相同。

後排地板中央凸起不是很高,同時後排空間的表現也比較寬裕,家用足夠。

至於動力系統,GS7將會搭載代號為4B20M1的2.0T發動機(和GS8是同款發動機)。最大功率201馬力,最大扭矩320,變速箱為6速手自一體,同時有4種變速箱模式可以切換。

其實看到這裏,GS7的身份已經很明確了,大致上就是一款縮小的GS8,但是可以肯定的是GS7的入門價肯定會更加地氣,對於那些不需要7座車型,同時覺得GS8有點貴的消費者,可以看一下GS7。

競爭對手

哈弗H7

指導價:14.98-19.38萬

H7是一款性價比很高的SUV,H7和H7L互相配合交出了不錯的銷售業績,不過GS7的帶來,肯定會帶給H7一些壓力的。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

把飛機上的引擎裝到摩托車上是什麼感覺?

首先現在的F1賽艇最輕的也有390公斤,由一個排量2500毫升的水星發動機或者排量3000毫升OMC發動機提供動力,由各船隊自己調整,能提供350馬力的動力,發動機轉速可以達到9500轉/分。從靜止狀態加速到100公里/小時僅需3。5秒,雖然馬力不是很多但是得益於極佳的馬力推重比,可以帶來如此變態的數據,而視頻中的小船估計全部重量就在那台發動機上了。

之前我們聊過星型發動機,原本是飛機上的東西愛好者將其裝在車上,形成一種別具一格的風格,今天我們看看不一樣的混搭。

MTT Y2K

星型發動機雖然很神奇,但那畢竟是上世紀的產物,而且由於風冷的結構,註定裝在摩托車上主要就是外形了。雖然聲音聽着還是很吊。

不過和下面這台相比就弱爆了。

Y2K來自美國的MTT公司,俗稱陸地噴氣機,大概知道怎麼回事了吧,因為Y2K直接應用勞斯萊斯的Allison 250-C18渦輪噴射機引擎,貝爾直升機的同款引擎,在52000轉(沒看錯)的時候爆發出320馬力。

與之匹配的是2速變速箱。該車的極速超過了400公里。不過相比其外形還有數據,最讓人驚訝的就是那飛機起飛一樣的聲音,來感受下。

TRD小船

以往我們看到改裝的發動機基本上用於日系車,不過這一次是用在了船上,還是一艘小木船。

首先現在的F1賽艇最輕的也有390公斤,由一個排量2500毫升的水星發動機或者排量3000毫升OMC發動機提供動力,由各船隊自己調整,能提供350馬力的動力,發動機轉速可以達到9500轉/分。

從靜止狀態加速到100公里/小時僅需3.5秒,雖然馬力不是很多但是得益於極佳的馬力推重比,可以帶來如此變態的數據,而視頻中的小船估計全部重量就在那台發動機上了。從細節處還看到了TRD的改裝件,不多說,直接看療效。

自行車動力的911:

911作為保時捷的靈魂車型,絕對是無數人的Dream Car,雖然其價格在跑車當中雖然不算貴,但也確實不便宜。

其實不僅僅在國內房價貴,由於不想等上五百年。所以國外玩民間的高手決定自己動手DIY一台911 GT3,你說要跑多快?沒必要,比走路快就行。

所以最後該車的動力來自己駕駛員自己,因為其骨子里是一台四輪自行車,但是外殼是911嘛,還是GT3。如果借不到500年的抓緊時間自己糊一個吧!

本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

怒肝倆月,新鮮出爐史上最有趣的Java小白手冊,第一版,每個 Java 初學者都應該收藏

這麼說吧,在我眼裡,Java 就是最流行的編程語言,沒有之一(PHP 往一邊站)。不僅崗位多,容易找到工作,關鍵是薪資水平也到位,不學 Java 虧得慌,對吧?

那可能零基礎學編程的小夥伴就會頭疼了,網上關於 Java 的大部分技術文章都不夠幽默,不夠風趣,不夠系列,急需要一份能看得進去的學習手冊,那我覺得我肝的這份手冊正好符合要求,並且會一直持續更新下去。

第一版的內容暫時包含兩方面,Java 基礎和 Java 面向對象編程。來吧,先上目錄,一睹為快。

01、Java 基本語法簡介
02、Java 基本數據類型簡介
03、Java main() 方法簡介
04、Java 的流程控制語句
05、Java 包的簡介
06、Java 到底是值傳遞還是引用傳遞
07、Java 的類和對象
08、Java 構造方法
09、Java 抽象類
10、Java 接口
11、Java 繼承
12、this 關鍵字
13、super 關鍵字
14、重寫和重載
15、static 關鍵字
16、Java 枚舉
17、final 關鍵字

目錄欣賞完了,接下來就是拜讀精華內容的時間,搬個小板凳,認認真真好好學吧,學到就是賺到!

一、Java 基本語法簡介

01、數據類型

Java 有 2 種數據類型,一種是基本數據類型,一種是引用類型。

基本數據類型用於存儲簡單類型的數據,比如說,int、long、byte、short 用於存儲整數,float、double 用於存儲浮點數,char 用於存儲字符,boolean 用於存儲布爾值。

不同的基本數據類型,有不同的默認值和大小,來個表格感受下。

數據類型 默認值 大小
boolean false 1比特
char ‘\u0000’ 2字節
byte 0 1字節
short 0 2字節
int 0 4字節
long 0L 8字節
float 0.0f 4字節
double 0.0 8字節

引用類型用於存儲對象(null 表示沒有值的對象)的引用,String 是引用類型的最佳代表,比如說 String cmower = "沉默王二"

02、聲明變量

要聲明一個變量,必須指定它的名字和類型,來看一個簡單的示例:

int age;
String name;

count 和 name 在聲明後會得到一個默認值,按照它們的數據類型——不能是局部變量(否則 Java 編譯器會在你使用變量的時候提醒要先賦值),必須是類成員變量。

public class SyntaxLocalVariable {
    int age;
    String name;

    public static void main(String[] args) {
        SyntaxLocalVariable syntax = new SyntaxLocalVariable();
        System.out.println(syntax.age); // 輸出 0
        System.out.println(syntax.name);  // 輸出 null
    }
}

也可以在聲明一個變量后使用“=”操作符進行賦值,就像下面這樣:

int age = 18;
String name = "沉默王二";

我們定義了 2 個變量,int 類型的 age 和 String 類型的 name,age 賦值 18,name 賦值為“沉默王二”。

每行代碼後面都跟了一個“;”,表示當前語句結束了。

在 Java 中,變量最好遵守命名約定,這樣能提高代碼的可閱讀性。

  • 以字母、下劃線(_)或者美元符號($)開頭
  • 不能使用 Java 的保留字,比如說 int 不能作為變量名

03、數組

數組在 Java 中佔據着重要的位置,它是很多集合類的底層實現。數組屬於引用類型,它用來存儲一系列指定類型的數據。

聲明數組的一般語法如下所示:

type[] identiier = new type[length];

type 可以是任意的基本數據類型或者引用類型。來看下面這個例子:

public class ArraysDemo {
    public static void main(String[] args) {
        int [] nums = new int[10];
        nums[0] = 18;
        nums[1] = 19;
        System.out.println(nums[0]);
    }
}

數組的索引從 0 開始,第一個元素的索引為 0,第二個元素的索引為 1。為什麼要這樣設計?感興趣的話,你可以去探究一下。

通過變量名[索引]的方式可以訪問數組指定索引處的元素,賦值或者取值是一樣的。

04、關鍵字

關鍵字屬於保留字,在 Java 中具有特殊的含義,比如說 public、final、static、new 等等,它們不能用來作為變量名。為了便於你作為參照,我列舉了 48 個常用的關鍵字,你可以瞅一瞅。

  1. abstract: abstract 關鍵字用於聲明抽象類——可以有抽象和非抽象方法。

  2. boolean: boolean 關鍵字用於將變量聲明為布爾值類型,它只有 true 和 false 兩個值。

  3. break: break 關鍵字用於中斷循環或 switch 語句。

  4. byte: byte 關鍵字用於聲明一個可以容納 8 個比特的變量。

  5. case: case 關鍵字用於在 switch 語句中標記條件的值。

  6. catch: catch 關鍵字用於捕獲 try 語句中的異常。

  7. char: char 關鍵字用於聲明一個可以容納無符號 16 位比特的 Unicode 字符的變量。

  8. class: class 關鍵字用於聲明一個類。

  9. continue: continue 關鍵字用於繼續下一個循環。它可以在指定條件下跳過其餘代碼。

  10. default: default 關鍵字用於指定 switch 語句中除去 case 條件之外的默認代碼塊。

  11. do: do 關鍵字通常和 while 關鍵字配合使用,do 后緊跟循環體。

  12. double: double 關鍵字用於聲明一個可以容納 64 位浮點數的變量。

  13. else: else 關鍵字用於指示 if 語句中的備用分支。

  14. enum: enum(枚舉)關鍵字用於定義一組固定的常量。

  15. extends: extends 關鍵字用於指示一個類是從另一個類或接口繼承的。

  16. final: final 關鍵字用於指示該變量是不可更改的。

  17. finally: finally 關鍵字和 try-catch 配合使用,表示無論是否處理異常,總是執行 finally 塊中的代碼。

  18. float: float 關鍵字用於聲明一個可以容納 32 位浮點數的變量。

  19. for: for 關鍵字用於啟動一個 for 循環,如果循環次數是固定的,建議使用 for 循環。

  20. if: if 關鍵字用於指定條件,如果條件為真,則執行對應代碼。

  21. implements: implements 關鍵字用於實現接口。

  22. import: import 關鍵字用於導入對應的類或者接口。

  23. instanceof: instanceof 關鍵字用於判斷對象是否屬於某個類型(class)。

  24. int: int 關鍵字用於聲明一個可以容納 32 位帶符號的整數變量。

  25. interface: interface 關鍵字用於聲明接口——只能具有抽象方法。

  26. long: long 關鍵字用於聲明一個可以容納 64 位整數的變量。

  27. native: native 關鍵字用於指定一個方法是通過調用本機接口(非 Java)實現的。

  28. new: new 關鍵字用於創建一個新的對象。

  29. null: 如果一個變量是空的(什麼引用也沒有指向),就可以將它賦值為 null。

  30. package: package 關鍵字用於聲明類所在的包。

  31. private: private 關鍵字是一個訪問修飾符,表示方法或變量只對當前類可見。

  32. protected: protected 關鍵字也是一個訪問修飾符,表示方法或變量對同一包內的類和所有子類可見。

  33. public: public 關鍵字是另外一個訪問修飾符,除了可以聲明方法和變量(所有類可見),還可以聲明類。main() 方法必須聲明為 public。

  34. return: return 關鍵字用於在代碼執行完成后返回(一個值)。

  35. short: short 關鍵字用於聲明一個可以容納 16 位整數的變量。

  36. static: static 關鍵字表示該變量或方法是靜態變量或靜態方法。

  37. strictfp: strictfp 關鍵字並不常見,通常用於修飾一個方法,確保方法體內的浮點數運算在每個平台上執行的結果相同。

  38. super: super 關鍵字可用於調用父類的方法或者變量。

  39. switch: switch 關鍵字通常用於三個(以上)的條件判斷。

  40. synchronized: synchronized 關鍵字用於指定多線程代碼中的同步方法、變量或者代碼塊。

  41. this: this 關鍵字可用於在方法或構造函數中引用當前對象。

  42. throw: throw 關鍵字主動拋出異常。

  43. throws: throws 關鍵字用於聲明異常。

  44. transient: transient 關鍵字在序列化的使用用到,它修飾的字段不會被序列化。

  45. try: try 關鍵字用於包裹要捕獲異常的代碼塊。

  46. void: void 關鍵字用於指定方法沒有返回值。

  47. volatile: volatile 關鍵字保證了不同線程對它修飾的變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。

  48. while: 如果循環次數不固定,建議使用 while 循環。

05、操作符

除去“=”賦值操作符,Java 中還有很多其他作用的操作符,我們來大致看一下。

①、算術運算符

  • +(加號)
  • –(減號)
  • *(乘號)
  • /(除號)
  • %(取余)

來看一個例子:

public class ArithmeticOperator {
    public static void main(String[] args) {
        int a = 10;
        int b = 5;

        System.out.println(a + b);//15  
        System.out.println(a - b);//5  
        System.out.println(a * b);//50  
        System.out.println(a / b);//2  
        System.out.println(a % b);//0  
    }
}

“+”號比較特殊,還可以用於字符串拼接,來看一個例子:

String result = "沉默王二" + "一枚有趣的程序員";

②、邏輯運算符

邏輯運算符通常用於布爾表達式,常見的有:

  • &&(AND)多個條件中只要有一個為 false 結果就為 false
  • ||(OR)多個條件只要有一個為 true 結果就為 true
  • !(NOT)條件如果為 true,加上“!”就為 false,否則,反之。

來看一個例子:

public class LogicalOperator {
    public static void main(String[] args) {
        int a=10;
        int b=5;
        int c=20;
        System.out.println(a<b&&a<c);//false
        System.out.println(a>b||a<c);//true
        System.out.println(!(a<b)); // true
    }
}

③、比較運算符

  • &lt; (小於)
  • &lt;= (小於或者等於)
  • &gt; (大於)
  • &gt;= (大於或者等於)
  • == (相等)
  • != (不等)

06、程序結構

Java 中最小的程序單元叫做類,一個類可以有一個或者多個字段(也叫作成員變量),還可以有一個或者多個方法,甚至還可以有一些內部類。

如果一個類想要執行,就必須有一個 main 方法——程序運行的入口,就好像人的嘴一樣,嗯,可以這麼牽強的理解一下。

public class StructureProgram {
    public static void main(String[] args) {
        System.out.println("沒有成員變量,只有一個 main 方法");
    }
}
  • 類名叫做 StructureProgram,在它裏面,只有一個 main 方法。
  • {} 之間的代碼稱之為代碼塊。
  • 以上源代碼將會保存在一個後綴名為 java 的文件中。

07、編譯然後執行代碼

通常,一些教程在介紹這塊內容的時候,建議你通過命令行中先執行 javac 命令將源代碼編譯成字節碼文件,然後再執行 java 命令指定代碼。

但我不希望這個糟糕的局面再繼續下去了——新手安裝配置 JDK 真的蠻需要勇氣和耐心的,稍有不慎,沒入門就先放棄了。況且,在命令行中編譯源代碼會遇到很多莫名其妙的錯誤,這對新手是極其致命的——如果你再遇到這種老式的教程,可以吐口水了。

好的方法,就是去下載 IntelliJ IDEA,簡稱 IDEA,它被業界公認為最好的 Java 集成開發工具,尤其在智能代碼助手、代碼自動提示、代碼重構、代碼版本管理(Git、SVN、Maven)、單元測試、代碼分析等方面有着亮眼的發揮。IDEA 產於捷克(位於東歐),開發人員以嚴謹著稱。IDEA 分為社區版和付費版兩個版本,新手直接下載社區版就足夠用了。

安裝成功后,可以開始敲代碼了,然後直接右鍵運行(連保存都省了),結果會在 Run 面板中显示,如下圖所示。

想查看反編譯后的字節碼的話,可以在 src 的同級目錄 target/classes 的包路徑下找到一個 StructureProgram.class 的文件(如果找不到的話,在目錄上右鍵選擇「Reload from Disk」)。

可以雙擊打開它。

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.cmower.baeldung.basic;

public class StructureProgram {
    public StructureProgram() {
    }

    public static void main(String[] args) {
        System.out.println("沒有成員變量,只有一個 main 方法");
    }
}

IDEA 默認會用 Fernflower 將 class 字節碼反編譯為我們可以看得懂的 Java 代碼。實際上,class 字節碼(請安裝 show bytecode 插件)長下面這個樣子:

// class version 57.65535 (-65479)
// access flags 0x21
public class com/cmower/baeldung/basic/StructureProgram {

  // compiled from: StructureProgram.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/cmower/baeldung/basic/StructureProgram; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 5 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream
;
    LDC "\u6ca1\u6709\u6210\u5458\u53d8\u91cf\uff0c\u53ea\u6709\u4e00\u4e2a main \u65b9\u6cd5"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 6 L1
    RETURN
   L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1
}

新手看起來還是有些懵逼的,建議過過眼癮就行了。

二、Java 基本數據類型簡介

01、布爾

布爾(boolean)僅用於存儲兩個值:true 和 false,也就是真和假,通常用於條件的判斷。代碼示例:

boolean flag = true;

02、byte

byte 的取值範圍在 -128 和 127 之間,包含 127。最小值為 -128,最大值為 127,默認值為 0。

在網絡傳輸的過程中,為了節省空間,常用字節來作為數據的傳輸方式。代碼示例:

byte a = 10;
byte b = -10;

03、short

short 的取值範圍在 -32,768 和 32,767 之間,包含 32,767。最小值為 -32,768,最大值為 32,767,默認值為 0。代碼示例:

short s = 10000;
short r = -5000;

04、int

int 的取值範圍在 -2,147,483,648(-2 ^ 31)和 2,147,483,647(2 ^ 31 -1)(含)之間,默認值為 0。如果沒有特殊需求,整形數據就用 int。代碼示例:

int a = 100000;
int b = -200000;

05、long

long 的取值範圍在 -9,223,372,036,854,775,808(-2^63) 和 9,223,372,036,854,775,807(2^63 -1)(含)之間,默認值為 0。如果 int 存儲不下,就用 long,整形數據就用 int。代碼示例:

long a = 100000L
long b = -200000L;

為了和 int 作區分,long 型變量在聲明的時候,末尾要帶上大寫的“L”。不用小寫的“l”,是因為小寫的“l”容易和数字“1”混淆。

06、float

float 是單精度的浮點數,遵循 IEEE 754(二進制浮點數算術標準),取值範圍是無限的,默認值為 0.0f。float 不適合用於精確的數值,比如說貨幣。代碼示例:

float f1 = 234.5f;

為了和 double 作區分,float 型變量在聲明的時候,末尾要帶上小寫的“f”。不需要使用大寫的“F”,是因為小寫的“f”很容易辨別。

07、double

double 是雙精度的浮點數,遵循 IEEE 754(二進制浮點數算術標準),取值範圍也是無限的,默認值為 0.0。double 同樣不適合用於精確的數值,比如說貨幣。代碼示例:

double d1 = 12.3

那精確的數值用什麼表示呢?最好使用 BigDecimal,它可以表示一個任意大小且精度完全準確的浮點數。針對貨幣類型的數值,也可以先乘以 100 轉成整形進行處理。

Tips:單精度是這樣的格式,1 位符號,8 位指數,23 位小數,有效位數為 7 位。

雙精度是這樣的格式,1 位符號,11 位指數,52 為小數,有效位數為 16 位。

取值範圍取決於指數位,計算精度取決於小數位(尾數)。小數位越多,則能表示的數越大,那麼計算精度則越高。

一個數由若干位数字組成,其中影響測量精度的数字稱作有效数字,也稱有效數位。有效数字指科學計算中用以表示一個浮點數精度的那些数字。一般地,指一個用小數形式表示的浮點數中,從第一個非零的数字算起的所有数字。如 1.24 和 0.00124 的有效数字都有 3 位。

08、char

char 可以表示一個 16 位的 Unicode 字符,其值範圍在 ‘\u0000’(0)和 ‘\uffff’(65,535)(包含)之間。代碼示例:

char letterA = 'A'// 用英文的單引號包裹住。

三、Java main() 方法簡介

每個程序都需要一個入口,對於 Java 程序來說,入口就是 main 方法。

public static void main(String[] args) {}

public、static、void 這 3 個關鍵字在前面的內容已經介紹過了,如果覺得回去找比較麻煩的話,這裏再貼一下:

  • public 關鍵字是另外一個訪問修飾符,除了可以聲明方法和變量(所有類可見),還可以聲明類。main() 方法必須聲明為 public。

  • static 關鍵字表示該變量或方法是靜態變量或靜態方法,可以直接通過類訪問,不需要實例化對象來訪問。

  • void 關鍵字用於指定方法沒有返回值。

另外,main 關鍵字為方法的名字,Java 虛擬機在執行程序時會尋找這個標識符;args 為 main() 方法的參數名,它的類型為一個 String 數組,也就是說,在使用 java 命令執行程序的時候,可以給 main() 方法傳遞字符串數組作為參數。

java HelloWorld 沉默王二 沉默王三

javac 命令用來編譯程序,java 命令用來執行程序,HelloWorld 為這段程序的類名,沉默王二和沉默王三為字符串數組,中間通過空格隔開,然後就可以在 main() 方法中通過 args[0]args[1] 獲取傳遞的參數值了。

public class HelloWorld {
    public static void main(String[] args) {
        if ("沉默王二".equals(args[0])) {

        }

        if ("沉默王三".equals(args[1])) {

        }
    }
}

main() 方法的寫法並不是唯一的,還有其他幾種變體,儘管它們可能並不常見,可以簡單來了解一下。

第二種,把方括號 [] 往 args 靠近而不是 String 靠近:

public static void main(String []args) { }

第三種,把方括號 [] 放在 args 的右側:

public static void main(String args[]) { }

第四種,還可以把數組形式換成可變參數的形式:

public static void main(String...args) { }

第五種,在 main() 方法上添加另外一個修飾符 strictfp,用於強調在處理浮點數時的兼容性:

public strictfp static void main(String[] args) { }

也可以在 main() 方法上添加 final 關鍵字或者 synchronized 關鍵字。

第六種,還可以為 args 參數添加 final 關鍵字:

public static void main(final String[] args) { }

第七種,最複雜的一種,所有可以添加的關鍵字統統添加上:

final static synchronized strictfp void main(final String[] args) { }

當然了,並不需要為了裝逼特意把 main() 方法寫成上面提到的這些形式,使用 IDE 提供的默認形式就可以了。

四、Java 的流程控制語句

在 Java 中,有三種類型的流程控制語句:

  • 條件分支,用於在兩個或者多個條件之間做出選擇,常見的有 if/else/else if、三元運算符和 switch 語句。

  • 循環或者遍歷,常見的有 for、while 和 do-while。

  • break 和 continue,用於跳出循環或者跳過進入下一輪循環。

if 語句

if 語句的格式如下:

if(布爾表達式){  
// 如果條件為 true,則執行這塊代碼

畫個流程圖表示一下:

來寫個示例:

public class IfExample {
    public static void main(String[] args) {
        int age = 20;
        if (age < 30) {
            System.out.println("青春年華");
        }
    }
}

輸出:

青春年華

if-else 語句

if-else 語句的格式如下:

if(布爾表達式){  
// 條件為 true 時執行的代碼塊
}else{  
// 條件為 false  時執行的代碼塊
}  

畫個流程圖表示一下:

來寫個示例:

public class IfElseExample {
    public static void main(String[] args) {
        int age = 31;
        if (age < 30) {
            System.out.println("青春年華");
        } else {
            System.out.println("而立之年");
        }
    }
}

輸出:

而立之年

除了這個例子之外,還有一個判斷閏年(被 4 整除但不能被 100 整除或者被 400 整除)的例子:

public class LeapYear {
    public static void main(String[] args) {
        int year = 2020;
        if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)) {
            System.out.println("閏年");
        } else {
            System.out.println("普通年份");
        }
    }
}

輸出:

閏年

如果執行語句比較簡單的話,可以使用三元運算符來代替 if-else 語句,如果條件為 true,返回 ? 後面 : 前面的值;如果條件為 false,返回 : 後面的值。

public class IfElseTernaryExample {
    public static void main(String[] args) {
        int num = 13;
        String result = (num % 2 == 0) ? "偶數" : "奇數";
        System.out.println(result);
    }
}

輸出:

奇數

if-else-if 語句

if-else-if 語句的格式如下:

if(條件1){  
// 條件1 為 true 時執行的代碼
}else if(條件2){  
// 條件2 為 true 時執行的代碼
}  
else if(條件3){  
// 條件3 為 true 時執行的代碼
}  
...  
else{  
// 以上條件均為 false 時執行的代碼

畫個流程圖表示一下:

來寫個示例:

public class IfElseIfExample {
    public static void main(String[] args) {
        int age = 31;
        if (age < 30) {
            System.out.println("青春年華");
        } else if (age >= 30 && age < 40 ) {
            System.out.println("而立之年");
        } else if (age >= 40 && age < 50 ) {
            System.out.println("不惑之年");
        } else {
            System.out.println("知天命");
        }
    }
}

輸出:

而立之年

if 嵌套語句

if 嵌套語句的格式如下:

if(外側條件){    
     // 外側條件為 true 時執行的代碼 
          if(內側條件){  
             // 內側條件為 true 時執行的代碼
    }    
}  

畫個流程圖表示一下:

來寫個示例:

public class NestedIfExample {
    public static void main(String[] args) {
        int age = 20;
        boolean isGirl = true;
        if (age >= 20) {
            if (isGirl) {
                System.out.println("女生法定結婚年齡");
            }
        }
    }
}

輸出:

女生法定結婚年齡

switch 語句的格式:

switch(變量) {    
case 可選值1:    
 // 可選值1匹配后執行的代碼;    
 break;  // 該關鍵字是可選項
case 可選值2:    
 // 可選值2匹配后執行的代碼;    
 break;  // 該關鍵字是可選項
......    

default// 該關鍵字是可選項     
 // 所有可選值都不匹配后執行的代碼 
}    
  • 變量可以有 1 個或者 N 個值。

  • 值類型必須和變量類型是一致的,並且值是確定的。

  • 值必須是唯一的,不能重複,否則編譯會出錯。

  • break 關鍵字是可選的,如果沒有,則執行下一個 case,如果有,則跳出 switch 語句。

  • default 關鍵字也是可選的。

畫個流程圖:

來個示例:

public class Switch1 {
    public static void main(String[] args) {
        int age = 20;
        switch (age) {
            case 20 :
                System.out.println("上學");
                break;
            case 24 :
                System.out.println("蘇州工作");
                break;
            case 30 :
                System.out.println("洛陽工作");
                break;
            default:
                System.out.println("未知");
                break// 可省略
        }
    }
}

輸出:

上學

當兩個值要執行的代碼相同時,可以把要執行的代碼寫在下一個 case 語句中,而上一個 case 語句中什麼也沒有,來看一下示例:

public class Switch2 {
    public static void main(String[] args) {
        String name = "沉默王二";
        switch (name) {
            case "詹姆斯":
                System.out.println("籃球運動員");
                break;
            case "穆里尼奧":
                System.out.println("足球教練");
                break;
            case "沉默王二":
            case "沉默王三":
                System.out.println("乒乓球愛好者");
                break;
            default:
                throw new IllegalArgumentException(
                        "名字沒有匹配項");

        }
    }
}

輸出:

乒乓球愛好者

枚舉作為 switch 語句的變量也很常見,來看例子:

public class SwitchEnumDemo {
    public enum PlayerTypes {
        TENNIS,
        FOOTBALL,
        BASKETBALL,
        UNKNOWN
    }

    public static void main(String[] args) {
        System.out.println(createPlayer(PlayerTypes.BASKETBALL));
    }

    private static String createPlayer(PlayerTypes playerType) {
        switch (playerType) {
            case TENNIS:
                return "網球運動員費德勒";
            case FOOTBALL:
                return "足球運動員C羅";
            case BASKETBALL:
                return "籃球運動員詹姆斯";
            case UNKNOWN:
                throw new IllegalArgumentException("未知");
            default:
                throw new IllegalArgumentException(
                        "運動員類型: " + playerType);

        }
    }
}

輸出:

籃球運動員詹姆斯

循環語句比較

比較方式 for while do-while
簡介 for 循環的次數是固定的 while 循環的次數是不固定的,並且需要條件為 true do-while 循環的次數也不固定,但會至少執行一次循環,無聊條件是否為 true
何時使用 循環次數固定的 循環次數是不固定的 循環次數不固定,並且循環體至少要執行一次
語法 for(init:condition;++/–) {// 要執行的代碼} while(condition){// 要執行的代碼} do{//要執行的代碼}while(condition);

普通的 for 循環

普通的 for 循環可以分為 4 個部分:

1)初始變量:循環開始執行時的初始條件。

2)條件:循環每次執行時要判斷的條件,如果為 true,就執行循環體;如果為 false,就跳出循環。當然了,條件是可選的,如果沒有條件,則會一直循環。

3)循環體:循環每次要執行的代碼塊,直到條件變為 false。

4)自增/自減:初識變量變化的方式。

來看一下普通 for 循環的格式:

for(初識變量;條件;自增/自減){  
// 循環體
}  

畫個流程圖:

來個示例:

public class ForExample {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            System.out.println("沉默王二好帥啊");
        }
    }
}

輸出:

沉默王二好帥啊
沉默王二好帥啊
沉默王二好帥啊
沉默王二好帥啊
沉默王二好帥啊

循環語句還可以嵌套呢,這樣就可以打印出更好玩的呢。

public class PyramidForExample {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            for (int j = 0;j<= i;j++) {
                System.out.print("");
            }
            System.out.println();
        }
    }
}

打印出什麼玩意呢?






for-each

for-each 循環通常用於遍曆數組和集合,它的使用規則比普通的 for 循環還要簡單,不需要初始變量,不需要條件,不需要下標來自增或者自減。來看一下語法:

for(元素類型 元素 : 數組或集合){  
// 要執行的代碼
}  

來看一下示例:

public class ForEachExample {
    public static void main(String[] args) {
        String[] strs = {"沉默王二""一枚有趣的程序員"};

        for (String str : strs) {
            System.out.println(str);
        }
    }
}

輸出:

沉默王二
一枚有趣的程序員

無限 for 循環

想不想體驗一下無限 for 循環的威力,也就是死循環?

public class InfinitiveForExample {
    public static void main(String[] args) {
        for(;;){
            System.out.println("停不下來。。。。");
        }
    }
}

輸出:

停不下來。。。。
停不下來。。。。
停不下來。。。。
停不下來。。。。

一旦運行起來,就停不下來了,除非強制停止。

while 循環

while(條件){  
//循環體  
}  

畫個流程圖:

來個示例:

public class WhileExample {
    public static void main(String[] args) {
        int i = 0;
        while (true) {
            System.out.println("沉默王二");
            i++;
            if (i == 5) {
                break;
            }
        }
    }
}

猜猜會輸出幾次?

沉默王二
沉默王二
沉默王二
沉默王二
沉默王二

do-while 循環

do{  
// 循環體
}while(提交);  

畫個流程圖:

來個示例:

public class DoWhileExample {
    public static void main(String[] args) {
        int i = 0;
        do {
            System.out.println("沉默王二");
            i++;
            if (i == 5) {
                break;
            }
        } while (true);
    }
}

程序輸出結果如下所示:

沉默王二
沉默王二
沉默王二
沉默王二
沉默王二

break

break 關鍵字通常用於中斷循環或 switch 語句,它在指定條件下中斷程序的當前流程。如果是內部循環,則僅中斷內部循環。

可以將 break 關鍵字用於所有類型循環語句中,比如說 for 循環,while 循環,以及 do-while 循環。

來畫個流程圖感受一下:

用在 for 循環中的示例:

for (int i = 1; i <= 10; i++) {
    if (i == 5) {
        break;
    }
    System.out.println(i);
}

用在嵌套 for 循環中的示例:

for (int i = 1; i <= 3; i++) {
    for (int j = 1; j <= 3; j++) {
        if (i == 2 && j == 2) {
            break;
        }
        System.out.println(i + " " + j);
    }
}

用在 while 循環中的示例:

int i = 1;
while (i <= 10) {
    if (i == 5) {
        i++;
        break;
    }
    System.out.println(i);
    i++;
}

用在 do-while 循環中的示例:

int j = 1;
do {
    if (j == 5) { 
        j++;
        break;
    }
    System.out.println(j);
    j++;
while (j <= 10);

continue

當我們需要在 for 循環或者 (do)while 循環中立即跳轉到下一個循環時,就可以使用 continue 關鍵字,通常用於跳過指定條件下的循環體,如果循環是嵌套的,僅跳過當前循環。

來個示例:

public class ContinueDemo {
    public static void main(String[] args) {
        for (int i = 1; i <= 10; i++) {
            if (i == 5) {
                // 使用 continue 關鍵字
                continue;// 5 將會被跳過
            }
            System.out.println(i);
        }
    }
}

輸出:

1
2
3
4
6
7
8
9
10

5 真的被跳過了。

再來個循環嵌套的例子。

public class ContinueInnerDemo {
    public static void main(String[] args) {
        for (int i = 1; i <= 3; i++) {
            for (int j = 1; j <= 3; j++) {
                if (i == 2 && j == 2) {
                    //  當i=2,j=2時跳過
                    continue;
                }
                System.out.println(i + " " + j);
            }
        }
    }
}

打印出什麼玩意呢?

1 1
1 2
1 3
2 1
2 3
3 1
3 2
3 3

“2 2” 沒有輸出,被跳過了。

再來看一下 while 循環時 continue 的使用示例:

public class ContinueWhileDemo {
    public static void main(String[] args) {
        int i = 1;
        while (i <= 10) {
            if (i == 5) {
                i++;
                continue;
            }
            System.out.println(i);
            i++;
        }
    }
}

輸出:

1
2
3
4
6
7
8
9
10

注意:如果把 if 條件中的“i++”省略掉的話,程序就會進入死循環,一直在 continue。

最後,再來看一下 do-while 循環時 continue 的使用示例:

public class ContinueDoWhileDemo {
    public static void main(String[] args) {
        int i=1;
        do{
            if(i==5){
                i++;
                continue;
            }
            System.out.println(i);
            i++;
        }while(i<=10);
    }
}

輸出:

1
2
3
4
6
7
8
9
10

注意:同樣的,如果把 if 條件中的“i++”省略掉的話,程序就會進入死循環,一直在 continue。

五、Java 包的簡介

在 Java 中,我們使用 package(包)對相關的類、接口和子包進行分組。這樣做的好處有:

  • 使相關類型更容易查找
  • 避免命名衝突,比如說 com.itwanger.Hello 和 com.itwangsan.Hello 不同
  • 通過包和訪問權限控制符來限定類的可見性

01、創建一個包

package com.itwanger;

可以使用 package 關鍵字來定義一個包名,需要注意的是,這行代碼必須處於一個類中的第一行。強烈建議在包中聲明類,不要缺省,否則就失去了包結構的帶來的好處。

包的命名應該遵守以下規則:

  • 應該全部是小寫字母
  • 可以包含多個單詞,單詞之間使用“.”連接,比如說 java.lang
  • 名稱由公司名或者組織名確定,採用倒序的方式,比如說,我個人博客的域名是 www.itwanger.com,所以我創建的包名是就是 com.itwanger.xxxx

每個包或者子包都在磁盤上有自己的目錄結構,如果 Java 文件時在 com.itwanger.xxxx 包下,那麼該文件所在的目錄結構就應該是 com->itwanger->xxxx

02、使用包

讓我們在名為 test 的子包里新建一個 Cmower 類:

package com.itwanger.test;

public class Cmower {
    private String name;
    private int age;
}

如果需要在另外一個包中使用 Cmower 類,就需要通過 import 關鍵字將其引入。有兩種方式可供選擇,第一種,使用 * 導入包下所有的類:

import com.itwanger.test.*;

第二種,使用類名導入該類:

import com.itwanger.test.Cmower;

Java 和第三方類庫提供了很多包可供使用,可以通過上述的方式導入類庫使用。

package com.itwanger.test;

import java.util.ArrayList;
import java.util.List;

public class CmowerTest {
    public static void main(String[] args) {
        List<Cmower> list = new ArrayList<>();
        list.add(new Cmower());
    }
}

03、全名

有時,我們可能會使用來自不同包下的兩個具有相同名稱的類。例如,我們可能同時使用 java.sql.Datejava.util.Date。當我們遇到命名衝突時,我們需要對至少一個類使用全名(包名+類名)。

List<com.itwanger.test.Cmower> list1 = new ArrayList<>();
list.add(new com.itwanger.test.Cmower());

六、Java 到底是值傳遞還是引用傳遞

將參數傳遞給方法有兩種常見的方式,一種是“值傳遞”,一種是“引用傳遞”。C 語言本身只支持值傳遞,它的衍生品 C++ 既支持值傳遞,也支持引用傳遞,而 Java 只支持值傳遞。

01、值傳遞 VS 引用傳遞

首先,我們必須要搞清楚,到底什麼是值傳遞,什麼是引用傳遞,否則,討論 Java 到底是值傳遞還是引用傳遞就顯得毫無意義。

當一個參數按照值的方式在兩個方法之間傳遞時,調用者和被調用者其實是用的兩個不同的變量——被調用者中的變量(原始值)是調用者中變量的一份拷貝,對它們當中的任何一個變量修改都不會影響到另外一個變量。

而當一個參數按照引用傳遞的方式在兩個方法之間傳遞時,調用者和被調用者其實用的是同一個變量,當該變量被修改時,雙方都是可見的。

Java 程序員之所以容易搞混值傳遞和引用傳遞,主要是因為 Java 有兩種數據類型,一種是基本類型,比如說 int,另外一種是引用類型,比如說 String。

基本類型的變量存儲的都是實際的值,而引用類型的變量存儲的是對象的引用——指向了對象在內存中的地址。值和引用存儲在 stack(棧)中,而對象存儲在 heap(堆)中。

之所以有這個區別,是因為:

  • 棧的優勢是,存取速度比堆要快,僅次於直接位於 CPU 中的寄存器。但缺點是,棧中的數據大小與生存周期必須是確定的。
  • 堆的優勢是可以動態地分配內存大小,生存周期也不必事先告訴編譯器,Java 的垃圾回收器會自動收走那些不再使用的數據。但由於要在運行時動態分配內存,存取速度較慢。

02、基本類型的參數傳遞

眾所周知,Java 有 8 種基本數據類型,分別是 int、long、byte、short、float、double 、char 和 boolean。它們的值直接存儲在棧中,每當作為參數傳遞時,都會將原始值(實參)複製一份新的出來,給形參用。形參將會在被調用方法結束時從棧中清除。

來看下面這段代碼:

public class PrimitiveTypeDemo {
    public static void main(String[] args) {
        int age = 18;
        modify(age);
        System.out.println(age);
    }

    private static void modify(int age1) {
        age1 = 30;
    }
}

1)main 方法中的 age 是基本類型,所以它的值 18 直接存儲在棧中。

2)調用 modify() 方法的時候,將為實參 age 創建一個副本(形參 age1),它的值也為 18,不過是在棧中的其他位置。

3)對形參 age 的任何修改都只會影響它自身而不會影響實參。

03、引用類型的參數傳遞

來看一段創建引用類型變量的代碼:

Writer writer = new Writer(18"沉默王二");

writer 是對象嗎?還是對象的引用?為了搞清楚這個問題,我們可以把上面的代碼拆分為兩行代碼:

Writer writer;
writer = new Writer(18"沉默王二");

假如 writer 是對象的話,就不需要通過 new 關鍵字創建對象了,對吧?那也就是說,writer 並不是對象,在“=”操作符執行之前,它僅僅是一個變量。那誰是對象呢?new Writer(18, "沉默王二"),它是對象,存儲於堆中;然後,“=”操作符將對象的引用賦值給了 writer 變量,於是 writer 此時應該叫對象引用,它存儲在棧中,保存了對象在堆中的地址。

每當引用類型作為參數傳遞時,都會創建一個對象引用(實參)的副本(形參),該形參保存的地址和實參一樣。

來看下面這段代碼:

public class ReferenceTypeDemo {
    public static void main(String[] args) {
        Writer a = new Writer(18);
        Writer b = new Writer(18);
        modify(a, b);

        System.out.println(a.getAge());
        System.out.println(b.getAge());
    }

    private static void modify(Writer a1, Writer b1) {
        a1.setAge(30);

        b1 = new Writer(18);
        b1.setAge(30);
    }
}

1)在調用 modify() 方法之前,實參 a 和 b 指向的對象是不一樣的,儘管 age 都為 18。

2)在調用 modify() 方法時,實參 a 和 b 都在棧中創建了一個新的副本,分別是 a1 和 b1,但指向的對象是一致的(a 和 a1 指向對象 a,b 和 b1 指向對象 b)。

3)在 modify() 方法中,修改了形參 a1 的 age 為 30,意味着對象 a 的 age 從 18 變成了 30,而實參 a 指向的也是對象 a,所以 a 的 age 也變成了 30;形參 b1 指向了一個新的對象,隨後 b1 的 age 被修改為 30。

修改 a1 的 age,意味着同時修改了 a 的 age,因為它們指向的對象是一個;修改 b1 的 age,對 b 卻沒有影響,因為它們指向的對象是兩個。

程序輸出的結果如下所示:

30
18

果然和我們的分析是吻合的。

七、Java 的類和對象

類和對象是 Java 中最基本的兩個概念,可以說撐起了面向對象編程(OOP)的一片天。對象可以是現實中看得見的任何物體(一隻特立獨行的豬),也可以是想象中的任何虛擬物體(能七十二變的孫悟空),Java 通過類(class)來定義這些物體,有什麼狀態(通過字段,或者叫成員變量定義,比如說豬的顏色是純色還是花色),有什麼行為(通過方法定義,比如說豬會吃,會睡覺)。

來,讓我來定義一個簡單的類給你看看。

public class Pig {
    private String color;

    public void eat() {
        System.out.println("吃");
    }
}

默認情況下,每個 Java 類都會有一個空的構造方法,儘管它在源代碼中是缺省的,但卻可以通過反編譯字節碼看到它。

public class Pig {
    private String color;

    public Pig() {
    }

    public void eat() {
        System.out.println("吃");
    }
}

沒錯,就是多出來的那個 public Pig() {},參數是空的,方法體是空的。我們可以通過 new 關鍵字利用這個構造方法來創建一個對象,代碼如下所示:

 Pig pig = new Pig();

當然了,我們也可以主動添加帶參的構造方法。

public class Pig {
    private String color;

    public Pig(String color) {
        this.color = color;
    }

    public void eat() {
        System.out.println("吃");
    }
}

這時候,再查看反編譯后的字節碼時,你會發現缺省的無參構造方法消失了——和源代碼一模一樣。

public class Pig {
    private String color;

    public Pig(String color) {
        this.color = color;
    }

    public void eat() {
        System.out.println("吃");
    }
}

這意味着無法通過 new Pig() 來創建對象了——編譯器會提醒你追加參數。

比如說你將代碼修改為 new Pig("純白色"),或者添加無參的構造方法。

public class Pig {
    private String color;

    public Pig(String color) {
        this.color = color;
    }

    public Pig() {
    }

    public void eat() {
        System.out.println("吃");
    }
}

使用無參構造方法創建的對象狀態默認值為 null(color 字符串為引用類型),如果是基本類型的話,默認值為對應基本類型的默認值,比如說 int 為 0,更詳細的見下圖。

(圖片中有一處錯誤,boolean 的默認值為 false)

接下來,我們來創建多個 Pig 對象,它的顏色各不相同。

public class PigTest {
    public static void main(String[] args) {
        Pig pigNoColor = new Pig();
        Pig pigWhite = new Pig("純白色");
        Pig pigBlack = new Pig("純黑色");
    }
}

你看,我們創建了 3 個不同花色的 Pig 對象,全部來自於一個類,由此可見類的重要性,只需要定義一次,就可以多次使用。

那假如我想改變對象的狀態呢?該怎麼辦?目前毫無辦法,因為沒有任何可以更改狀態的方法,直接修改 color 是行不通的,因為它的訪問權限修飾符是 private 的。

最好的辦法就是為 Pig 類追加 getter/setter 方法,就像下面這樣:

public String getColor() {
    return color;
}

public void setColor(String color) {
    this.color = color;
}

通過 setColor() 方法來修改,通過 getColor() 方法獲取狀態,它們的權限修飾符是 public 的。

Pig pigNoColor = new Pig();
pigNoColor.setColor("花色");
System.out.println(pigNoColor.getColor()); // 花色

為什麼要這樣設計呢?可以直接將 color 字段的訪問權限修飾符換成是 public 的啊,不就和 getter/setter 一樣的效果了嗎?

因為有些情況,某些字段是不允許被隨意修改的,它只有在對象創建的時候初始化一次,比如說豬的年齡,它只能每年長一歲(舉個例子),沒有月光寶盒讓它變回去。

private int age;

public int getAge() {
    return age;
}

public void increaseAge() {
    this.age++;
}

你看,age 就沒有 setter 方法,只有一個每年可以調用一次的 increaseAge() 方法和 getter 方法。如果把 age 的訪問權限修飾符更改為 public,age 就完全失去控制了,可以隨意將其重置為 0 或者負數。

訪問權限修飾符對於 Java 來說,非常重要,目前共有四種:public、private、protected 和 default(缺省)。

一個類只能使用 public 或者 default 修飾,public 修飾的類你之前已經見到過了,現在我來定義一個缺省權限修飾符的類給你欣賞一下。

class Dog {
}

哈哈,其實也沒啥可以欣賞的。缺省意味着這個類可以被同一個包下的其他類進行訪問;而 public 意味着這個類可以被所有包下的類進行訪問。

假如硬要通過 private 和 protected 來修飾類的話,編譯器會生氣的,它不同意。

private 可以用來修飾類的構造方法、字段和方法,只能被當前類進行訪問。protected 也可以用來修飾類的構造方法、字段和方法,但它的權限範圍更寬一些,可以被同一個包中的類進行訪問,或者當前類的子類。

可以通過下面這張圖來對比一下四個權限修飾符之間的差別:

  • 同一個類中,不管是哪種權限修飾符,都可以訪問;
  • 同一個包下,private 修飾的無法訪問;
  • 子類可以訪問 public 和 protected 修飾的;
  • public 修飾符面向世界,哈哈,可以被所有的地方訪問到。

八、Java 構造方法

假設現在有一個 Writer 類,它有兩個字段,姓名和年紀:

public class Writer {
    private String name;
    private int age;

    @Override
    public String toString() {
        return "Writer{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

重寫了 toString() 方法,用於打印 Writer 類的詳情。由於沒有構造方法,意味着當我們創建 Writer 對象時,它的字段值並沒有初始化:

Writer writer = new Writer();
System.out.println(writer.toString());

輸出結果如下所示:

Writer{name='null', age=0}

name 是字符串類型,所以默認值為 null,age 為 int 類型,所以默認值為 0。

讓我們為 Writer 類主動加一個無參的構造方法:

public Writer() {
    this.name = "";
    this.age = 0;
}

構造方法也是一個方法,只不過它沒有返回值,默認返回創建對象的類型。需要注意的是,當前構造方法沒有參數,它被稱為無參構造方法。如果我們沒有主動創建無參構造方法的話,編譯器會隱式地自動添加一個無參的構造方法。這就是為什麼,一開始雖然沒有構造方法,卻可以使用 new Writer() 創建對象的原因,只不過,所有的字段都被初始化成了默認值。

接下來,讓我們添加一個有參的構造方法:

public Writer(String name, int age) {
    this.name = name;
    this.age = age;
}

現在,我們創建 Writer 對象的時候就可以通過對字段值初始化值了。

Writer writer1 = new Writer("沉默王二",18);
System.out.println(writer1.toString());

來看一下打印結果:

Writer{name='沉默王二', age=18}

可以根據字段的數量添加不同參數數量的構造方法,比如說,我們可以單獨為 name 字段添加一個構造方法:

public Writer(String name) {
    this.name = name;
}

為了能夠兼顧 age 字段,我們可以通過 this 關鍵字調用其他的構造方法:

public Writer(String name) {
    this(name,18);
}

把作者的年齡都默認初始化為 18。如果需要使用父類的構造方法,還可以使用 super 關鍵字,手冊後面有詳細的介紹。

九、Java 抽象類

當我們要完成的任務是確定的,但具體的方式需要隨後開個會投票的話,Java 的抽象類就派上用場了。這句話怎麼理解呢?搬個小板凳坐好,聽我來給你講講。

01、抽象類的 5 個關鍵點

1)定義抽象類的時候需要用到關鍵字 abstract,放在 class 關鍵字前。

public abstract class AbstractPlayer {
}

關於抽象類的命名,阿里出品的 Java 開發手冊上有強調,“抽象類命名要使用 Abstract 或 Base 開頭”,記住了哦。

2)抽象類不能被實例化,但可以有子類。

嘗試通過 new 關鍵字實例化的話,編譯器會報錯,提示“類是抽象的,不能實例化”。

通過 extends 關鍵字可以繼承抽象類,繼承后,BasketballPlayer 類就是 AbstractPlayer 的子類。

public class BasketballPlayer extends AbstractPlayer {
}

3)如果一個類定義了一個或多個抽象方法,那麼這個類必須是抽象類。

當在一個普通類(沒有使用 abstract 關鍵字修飾)中定義了抽象方法,編譯器就會有兩處錯誤提示。

第一處在類級別上,提醒你“這個類必須通過 abstract 關鍵字定義”,or 的那個信息沒必要,見下圖。

第二處在方法級別上,提醒你“抽象方法所在的類不是抽象的”,見下圖。

4)抽象類可以同時聲明抽象方法和具體方法,也可以什麼方法都沒有,但沒必要。就像下面這樣:

public abstract class AbstractPlayer {
    abstract void play();

    public void sleep() {
        System.out.println("運動員也要休息而不是挑戰極限");
    }
}

5)抽象類派生的子類必須實現父類中定義的抽象方法。比如說,抽象類中定義了 play() 方法,子類中就必須實現。

public class BasketballPlayer extends AbstractPlayer {
    @Override
    void play() {
        System.out.println("我是張伯倫,籃球場上得過 100 分");
    }
}

如果沒有實現的話,編譯器會提醒你“子類必須實現抽象方法”,見下圖。

02、什麼時候用抽象類

與抽象類息息相關的還有一個概念,就是接口,我們留到下一篇文章中詳細說,因為要說的知識點還是蠻多的。你現在只需要有這樣一個概念就好,接口是對行為的抽象,抽象類是對整個類(包含成員變量和行為)進行抽象。

(是不是有點明白又有點不明白,別著急,翹首以盼地等下一篇文章出爐吧)

除了接口之外,還有一個概念就是具體的類,就是不通過 abstract 修飾的普通類,見下面這段代碼中的定義。

public class BasketballPlayer {
   public void play() {
        System.out.println("我是詹姆斯,現役第一人");
    }
}

有接口,有具體類,那什麼時候該使用抽象類呢?

1)我們希望一些通用的功能被多個子類復用。比如說,AbstractPlayer 抽象類中有一個普通的方法 sleep(),表明所有運動員都需要休息,那麼這個方法就可以被子類復用。

public abstract class AbstractPlayer {
    public void sleep() {
        System.out.println("運動員也要休息而不是挑戰極限");
    }
}

雖然 AbstractPlayer 類可以不是抽象類——把 abstract 修飾符去掉也能滿足這種場景。但 AbstractPlayer 類可能還會有一個或者多個抽象方法。

BasketballPlayer 繼承了 AbstractPlayer 類,也就擁有了 sleep() 方法。

public class BasketballPlayer extends AbstractPlayer {
}

BasketballPlayer 對象可以直接調用 sleep() 方法:

BasketballPlayer basketballPlayer = new BasketballPlayer();
basketballPlayer.sleep();

FootballPlayer 繼承了 AbstractPlayer 類,也就擁有了 sleep() 方法。

public class FootballPlayer extends AbstractPlayer {
}

FootballPlayer 對象也可以直接調用 sleep() 方法:

FootballPlayer footballPlayer = new FootballPlayer();
footballPlayer.sleep();

2)我們需要在抽象類中定義好 API,然後在子類中擴展實現。比如說,AbstractPlayer 抽象類中有一個抽象方法 play(),定義所有運動員都可以從事某項運動,但需要對應子類去擴展實現。

public abstract class AbstractPlayer {
    abstract void play();
}

BasketballPlayer 繼承了 AbstractPlayer 類,擴展實現了自己的 play() 方法。

public class BasketballPlayer extends AbstractPlayer {
    @Override
    void play() {
        System.out.println("我是張伯倫,我籃球場上得過 100 分,");
    }
}

FootballPlayer 繼承了 AbstractPlayer 類,擴展實現了自己的 play() 方法。

public class FootballPlayer extends AbstractPlayer {
    @Override
    void play() {
        System.out.println("我是C羅,我能接住任意高度的頭球");
    }
}

3)如果父類與子類之間的關係符合 is-a 的層次關係,就可以使用抽象類,比如說籃球運動員是運動員,足球運動員是運動員。

03、具體示例

為了進一步展示抽象類的特性,我們再來看一個具體的示例。假設現在有一個文件,裏面的內容非常簡單——“Hello World”,現在需要有一個讀取器將內容讀取出來,最好能按照大寫的方式,或者小寫的方式。

這時候,最好定義一個抽象類,比如說 BaseFileReader:

public abstract class BaseFileReader {
    protected Path filePath;

    protected BaseFileReader(Path filePath) {
        this.filePath = filePath;
    }

    public List<String> readFile() throws IOException {
        return Files.lines(filePath)
                .map(this::mapFileLine).collect(Collectors.toList());
    }

    protected abstract String mapFileLine(String line);
}

filePath 為文件路徑,使用 protected 修飾,表明該成員變量可以在需要時被子類訪問。

readFile() 方法用來讀取文件,方法體裏面調用了抽象方法 mapFileLine()——需要子類擴展實現大小寫的方式。

你看,BaseFileReader 設計的就非常合理,並且易於擴展,子類只需要專註於具體的大小寫實現方式就可以了。

小寫的方式:

public class LowercaseFileReader extends BaseFileReader {
    protected LowercaseFileReader(Path filePath) {
        super(filePath);
    }

    @Override
    protected String mapFileLine(String line) {
        return line.toLowerCase();
    }
}

大寫的方式:

public class UppercaseFileReader extends BaseFileReader {
    protected UppercaseFileReader(Path filePath) {
        super(filePath);
    }

    @Override
    protected String mapFileLine(String line) {
        return line.toUpperCase();
    }
}

你看,從文件裏面一行一行讀取內容的代碼被子類復用了——抽象類 BaseFileReader 類中定義的普通方法 readFile()。與此同時,子類只需要專註於自己該做的工作,LowercaseFileReader 以小寫的方式讀取文件內容,UppercaseFileReader 以大寫的方式讀取文件內容。

接下來,我們來新建一個測試類 FileReaderTest:

public class FileReaderTest {
    public static void main(String[] args) throws URISyntaxException, IOException {
        URL location = FileReaderTest.class.getClassLoader().getResource("helloworld.txt");
        Path path = Paths.get(location.toURI());
        BaseFileReader lowercaseFileReader = new LowercaseFileReader(path);
        BaseFileReader uppercaseFileReader = new UppercaseFileReader(path);
        System.out.println(lowercaseFileReader.readFile());
        System.out.println(uppercaseFileReader.readFile());
    }
}

項目的 resource 目錄下有一個文本文件,名字叫 helloworld.txt。

可以通過 ClassLoader.getResource() 的方式獲取到該文件的 URI 路徑,然後就可以使用 LowercaseFileReader 和 UppercaseFileReader 兩種方式讀取到文本內容了。

輸出結果如下所示:

[hello world]
[HELLO WORLD]

十、Java 接口

對於面向對象編程來說,抽象是一個極具魅力的特徵。如果一個程序員的抽象思維很差,那他在編程中就會遇到很多困難,無法把業務變成具體的代碼。在 Java 中,可以通過兩種形式來達到抽象的目的,一種是抽象類,另外一種就是接口。

如果你現在就想知道抽象類與接口之間的區別,我可以提前給你說一個:

  • 一個類只能繼承一個抽象類,但卻可以實現多個接口。

當然了,在沒有搞清楚接口到底是什麼,它可以做什麼之前,這個區別理解起來會有點難度。

01、接口是什麼

接口是通過 interface 關鍵字定義的,它可以包含一些常量和方法,來看下面這個示例。

public interface Electronic {
    // 常量
    String LED = "LED";

    // 抽象方法
    int getElectricityUse();

    // 靜態方法
    static boolean isEnergyEfficient(String electtronicType) {
        return electtronicType.equals(LED);
    }

    // 默認方法
    default void printDescription() {
        System.out.println("电子");
    }
}

1)接口中定義的變量會在編譯的時候自動加上 public static final 修飾符,也就是說 LED 變量其實是一個常量。

Java 官方文檔上有這樣的聲明:

Every field declaration in the body of an interface is implicitly public, static, and final.

換句話說,接口可以用來作為常量類使用,還能省略掉 public static final,看似不錯的一種選擇,對吧?

不過,這種選擇並不可取。因為接口的本意是對方法進行抽象,而常量接口會對子類中的變量造成命名空間上的“污染”。

2)沒有使用 privatedefault 或者 static 關鍵字修飾的方法是隱式抽象的,在編譯的時候會自動加上 public abstract 修飾符。也就是說 getElectricityUse() 其實是一個抽象方法,沒有方法體——這是定義接口的本意。

3)從 Java 8 開始,接口中允許有靜態方法,比如說 isEnergyEfficient() 方法。

靜態方法無法由(實現了該接口的)類的對象調用,它只能通過接口的名字來調用,比如說 Electronic.isEnergyEfficient("LED")

接口中定義靜態方法的目的是為了提供一種簡單的機制,使我們不必創建對象就能調用方法,從而提高接口的競爭力。

4)接口中允許定義 default 方法也是從 Java 8 開始的,比如說 printDescription(),它始終由一個代碼塊組成,為實現該接口而不覆蓋該方法的類提供默認實現,也就是說,無法直接使用一個“;”號來結束默認方法——編譯器會報錯的。

允許在接口中定義默認方法的理由是很充分的,因為一個接口可能有多個實現類,這些類就必須實現接口中定義的抽象類,否則編譯器就會報錯。假如我們需要在所有的實現類中追加某個具體的方法,在沒有 default 方法的幫助下,我們就必須挨個對實現類進行修改。

來看一下 Electronic 接口反編譯后的字節碼吧,你會發現,接口中定義的所有變量或者方法,都會自動添加上 public 關鍵字——假如你想知道編譯器在背後都默默做了哪些輔助,記住反編譯字節碼就對了。

public interface Electronic
{

    public abstract int getElectricityUse();

    public static boolean isEnergyEfficient(String electtronicType)
    
{
        return electtronicType.equals("LED");
    }

    public void printDescription()
    
{
        System.out.println("\u7535\u5B50");
    }

    public static final String LED = "LED";
}

有些讀者可能會問,“二哥,為什麼我反編譯后的字節碼和你的不一樣,你用了什麼反編譯工具?”其實沒有什麼秘密,微信搜「沉默王二」回復關鍵字「JAD」就可以免費獲取了,超級好用。

02、定義接口的注意事項

由之前的例子我們就可以得出下面這些結論:

  • 接口中允許定義變量
  • 接口中允許定義抽象方法
  • 接口中允許定義靜態方法(Java 8 之後)
  • 接口中允許定義默認方法(Java 8 之後)

除此之外,我們還應該知道:

1)接口不允許直接實例化。

需要定義一個類去實現接口,然後再實例化。

public class Computer implements Electronic {

    public static void main(String[] args) {
        new Computer();
    }

    @Override
    public int getElectricityUse() {
        return 0;
    }
}

2)接口可以是空的,既不定義變量,也不定義方法。

public interface Serializable {
}

Serializable 是最典型的一個空的接口,我之前分享過一篇文章《Java Serializable:明明就一個空的接口嘛》,感興趣的讀者可以去我的個人博客看一看,你就明白了空接口的意義。

http://www.itwanger.com/java/2019/11/14/java-serializable.html

3)不要在定義接口的時候使用 final 關鍵字,否則會報編譯錯誤,因為接口就是為了讓子類實現的,而 final 阻止了這種行為。

4)接口的抽象方法不能是 private、protected 或者 final。

5)接口的變量是隱式 public static final,所以其值無法改變。

03、接口可以做什麼

1)使某些實現類具有我們想要的功能,比如說,實現了 Cloneable 接口的類具有拷貝的功能,實現了 Comparable 或者 Comparator 的類具有比較功能。

Cloneable 和 Serializable 一樣,都屬於標記型接口,它們內部都是空的。實現了 Cloneable 接口的類可以使用 Object.clone() 方法,否則會拋出 CloneNotSupportedException。

public class CloneableTest implements Cloneable {
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        CloneableTest c1 = new CloneableTest();
        CloneableTest c2 = (CloneableTest) c1.clone();
    }
}

運行后沒有報錯。現在把 implements Cloneable 去掉。

public class CloneableTest {
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        CloneableTest c1 = new CloneableTest();
        CloneableTest c2 = (CloneableTest) c1.clone();

    }
}

運行后拋出 CloneNotSupportedException:

Exception in thread "main" java.lang.CloneNotSupportedException: com.cmower.baeldung.interface1.CloneableTest
    at java.base/java.lang.Object.clone(Native Method)
    at com.cmower.baeldung.interface1.CloneableTest.clone(CloneableTest.java:6)
    at com.cmower.baeldung.interface1.CloneableTest.main(CloneableTest.java:11)

至於 Comparable 和 Comparator 的用法,感興趣的讀者可以參照我之前寫的另外一篇文章《來吧,一文徹底搞懂Java中的Comparable和Comparator》。

http://www.itwanger.com/java/2020/01/04/java-comparable-comparator.html

2)Java 原則上只支持單一繼承,但通過接口可以實現多重繼承的目的。

可能有些讀者會問,“二哥,為什麼 Java 只支持單一繼承?”簡單來解釋一下。

如果有兩個類共同繼承(extends)一個有特定方法的父類,那麼該方法會被兩個子類重寫。然後,如果你決定同時繼承這兩個子類,那麼在你調用該重寫方法時,編譯器不能識別你要調用哪個子類的方法。這也正是著名的菱形問題,見下圖。

ClassC 同時繼承了 ClassA 和 ClassB,ClassC 的對象在調用 ClassA 和 ClassB 中重載的方法時,就不知道該調用 ClassA 的方法,還是 ClassB 的方法。

接口沒有這方面的困擾。來定義兩個接口,Fly 會飛,Run 會跑。

public interface Fly {
    void fly();
}
public interface Run {
    void run();
}

然後讓一個類同時實現這兩個接口。

public class Pig implements Fly,Run{
    @Override
    public void fly() {
        System.out.println("會飛的豬");
    }

    @Override
    public void run() {
        System.out.println("會跑的豬");
    }
}

這就在某種形式上達到了多重繼承的目的:現實世界里,豬的確只會跑,但在雷軍的眼裡,站在風口的豬就會飛,這就需要賦予這隻豬更多的能力,通過抽象類是無法實現的,只能通過接口。

3)實現多態。

什麼是多態呢?通俗的理解,就是同一個事件發生在不同的對象上會產生不同的結果,鼠標左鍵點擊窗口上的 X 號可以關閉窗口,點擊超鏈接卻可以打開新的網頁。

多態可以通過繼承(extends)的關係實現,也可以通過接口的形式實現。來看這樣一個例子。

Shape 是表示一個形狀。

public interface Shape {
    String name();
}

圓是一個形狀。

public class Circle implements Shape {
    @Override
    public String name() {
        return "圓";
    }
}

正方形也是一個形狀。

public class Square implements Shape {
    @Override
    public String name() {
        return "正方形";
    }
}

然後來看測試類。

List<Shape> shapes = new ArrayList<>();
Shape circleShape = new Circle();
Shape squareShape = new Square();

shapes.add(circleShape);
shapes.add(squareShape);

for (Shape shape : shapes) {
    System.out.println(shape.name());
}

多態的存在 3 個前提:

1、要有繼承關係,Circle 和 Square 都實現了 Shape 接口
2、子類要重寫父類的方法,Circle 和 Square 都重寫了 name() 方法
3、父類引用指向子類對象,circleShape 和 squareShape 的類型都為 Shape,但前者指向的是 Circle 對象,後者指向的是 Square 對象。

然後,我們來看一下測試結果:


正方形

也就意味着,儘管在 for 循環中,shape 的類型都為 Shape,但在調用 name() 方法的時候,它知道 Circle 對象應該調用 Circle 類的 name() 方法,Square 對象應該調用 Square 類的 name() 方法。

04、接口與抽象類的區別

好了,關於接口的一切,你應該都搞清楚了。現在回到讀者春夏秋冬的那條留言,“兄弟,說說抽象類和接口之間的區別?”

1)語法層面上

  • 接口中不能有 public 和 protected 修飾的方法,抽象類中可以有。
  • 接口中的變量只能是隱式的常量,抽象類中可以有任意類型的變量。
  • 一個類只能繼承一個抽象類,但卻可以實現多個接口。

2)設計層面上

抽象類是對類的一種抽象,繼承抽象類的類和抽象類本身是一種 is-a 的關係。

接口是對類的某種行為的一種抽象,接口和類之間並沒有很強的關聯關係,所有的類都可以實現 Serializable 接口,從而具有序列化的功能。

就這麼多吧,能說道這份上,我相信面試官就不會為難你了。

十一、Java 繼承

在 Java 中,一個類可以繼承另外一個類或者實現多個接口,我想這一點,大部分的讀者應該都知道了。還有一點,我不確定大家是否知道,就是一個接口也可以繼承另外一個接口,就像下面這樣:

public interface OneInterface extends Cloneable {
}

這樣做有什麼好處呢?我想有一部分讀者應該已經猜出來了,就是實現了 OneInterface 接口的類,也可以使用 Object.clone() 方法了。

public class TestInterface implements OneInterface {
    public static void main(String[] args) throws CloneNotSupportedException {
        TestInterface c1 = new TestInterface();
        TestInterface c2 = (TestInterface) c1.clone();
    }
}

除此之外,我們還可以在 OneInterface 接口中定義其他一些抽象方法(比如說深拷貝),使該接口擁有 Cloneable 所不具有的功能。

public interface OneInterface extends Cloneable {
    void deepClone();
}

看到了吧?這就是繼承的好處:子接口擁有了父接口的方法,使得子接口具有了父接口相同的行為;同時,子接口還可以在此基礎上自由發揮,添加屬於自己的行為

以上,把“接口”換成“類”,結論同樣成立。讓我們來定義一個普通的父類 Wanger:

public class Wanger {
    int age;
    String name;
    void write() {
        System.out.println("我寫了本《基督山伯爵》");
    }
}

然後,我們再來定義一個子類 Wangxiaoer,使用關鍵字 extends 來繼承父類 Wanger:

public class Wangxiaoer extends Wanger{
    @Override
    void write() {
        System.out.println("我寫了本《茶花女》");
    }
}

我們可以將通用的方法和成員變量放在父類中,達到代碼復用的目的;然後將特殊的方法和成員變量放在子類中,除此之外,子類還可以覆蓋父類的方法(比如write() 方法)。這樣,子類也就煥發出了新的生命力。

Java 只支持單一繼承,這一點,我在上一篇接口的文章中已經提到過了。如果一個類在定義的時候沒有使用 extends 關鍵字,那麼它隱式地繼承了 java.lang.Object 類——在我看來,這恐怕就是 Java 號稱萬物皆對象的真正原因了。

那究竟子類繼承了父類的什麼呢?

子類可以繼承父類的非 private 成員變量,為了驗證這一點,我們來看下面這個示例。

public class Wanger {
    String defaultName;
    private String privateName;
    public String publicName;
    protected String protectedName;
}

父類 Wanger 定義了四種類型的成員變量,缺省的 defaultName、私有的 privateName、共有的 publicName、受保護的 protectedName。

在子類 Wangxiaoer 中定義一個測試方法 testVariable()

可以確認,除了私有的 privateName,其他三種類型的成員變量都可以繼承到。

同理,子類可以繼承父類的非 private 方法,為了驗證這一點,我們來看下面這個示例。

public class Wanger {
    void write() {
    }

    private void privateWrite() {
    }

    public void publicWrite() {
    }

    protected void protectedWrite() {
    }
}

父類 Wanger 定義了四種類型的方法,缺省的 write、私有的 privateWrite()、共有的 publicWrite()、受保護的 protectedWrite()。

在子類 Wangxiaoer 中定義一個 main 方法,並使用 new 關鍵字新建一個子類對象:

可以確認,除了私有的 privateWrite(),其他三種類型的方法都可以繼承到。

不過,子類無法繼承父類的構造方法。如果父類的構造方法是帶有參數的,代碼如下所示:

public class Wanger {
    int age;
    String name;

    public Wanger(int age, String name) {
        this.age = age;
        this.name = name;
    }
}

則必須在子類的構造器中顯式地通過 super 關鍵字進行調用,否則編譯器將提示以下錯誤:

修復后的代碼如下所示:

public class Wangxiaoer extends Wanger{
    public Wangxiaoer(int age, String name) {
        super(age, name);
    }
}

is-a 是繼承的一個明顯特徵,就是說子類的對象引用類型可以是一個父類類型。

public class Wangxiaoer extends Wanger{
    public static void main(String[] args) {
        Wanger wangxiaoer = new Wangxiaoer();
    }
}

同理,子接口的實現類的對象引用類型也可以是一個父接口類型。

public interface OneInterface extends Cloneable {
}
public class TestInterface implements OneInterface {
    public static void main(String[] args) {
        Cloneable c1 = new TestInterface();
    }
}

儘管一個類只能繼承一個類,但一個類卻可以實現多個接口,這一點,我在上一篇文章也提到過了。另外,還有一點我也提到了,就是 Java 8 之後,接口中可以定義 default 方法,這很方便,但也帶來了新的問題:

如果一個類實現了多個接口,而這些接口中定義了相同簽名的 default 方法,那麼這個類就要重寫該方法,否則編譯無法通過。

FlyInterface 是一個會飛的接口,裏面有一個簽名為 sleep() 的默認方法:

public interface FlyInterface {
    void fly();
    default void sleep() {
        System.out.println("睡着飛");
    }
}

RunInterface 是一個會跑的接口,裏面也有一個簽名為 sleep() 的默認方法:

public interface RunInterface {
    void run();
    default void sleep() {
        System.out.println("睡着跑");
    }
}

Pig 類實現了 FlyInterface 和 RunInterface 兩個接口,但這時候編譯出錯了。

原本,default 方法就是為實現該接口而不覆蓋該方法的類提供默認實現的,現在,相同方法簽名的 sleep() 方法把編譯器搞懵逼了,只能重寫了。

public class Pig implements FlyInterfaceRunInterface {

    @Override
    public void fly() {
        System.out.println("會飛的豬");
    }

    @Override
    public void sleep() {
        System.out.println("只能重寫了");
    }

    @Override
    public void run() {
        System.out.println("會跑的豬");
    }
}

類雖然不能繼承多個類,但接口卻可以繼承多個接口,這一點,我不知道有沒有觸及到一些讀者的知識盲區。

public interface WalkInterface extends FlyInterface,RunInterface{
    void walk();
}

十二、this 關鍵字

在 Java 中,this 關鍵字指的是當前對象(它的方法正在被調用)的引用,能理解吧,各位親?不理解的話,我們繼續往下看。

看完再不明白,你過來捶爆我,我保證不還手,只要不打臉。

01、消除字段歧義

我敢賭一毛錢,所有的讀者,不管男女老少,應該都知道這種用法,畢竟寫構造方法的時候經常用啊。誰要不知道,過來,我給你發一毛錢紅包,只要你臉皮夠厚。

public class Writer {
    private int age;
    private String name;

    public Writer(int age, String name) {
        this.age = age;
        this.name = name;
    }
}

Writer 類有兩個成員變量,分別是 age 和 name,在使用有參構造函數的時候,如果參數名和成員變量的名字相同,就需要使用 this 關鍵字消除歧義:this.age 是指成員變量,age 是指構造方法的參數。

02、引用類的其他構造方法

當一個類的構造方法有多個,並且它們之間有交集的話,就可以使用 this 關鍵字來調用不同的構造方法,從而減少代碼量。

比如說,在無參構造方法中調用有參構造方法:

public class Writer {
    private int age;
    private String name;

    public Writer(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public Writer() {
        this(18"沉默王二");
    }
}

也可以在有參構造方法中調用無參構造方法:

public class Writer {
    private int age;
    private String name;

    public Writer(int age, String name) {
        this();
        this.age = age;
        this.name = name;
    }

    public Writer() {
    }
}

需要注意的是,this() 必須是構造方法中的第一條語句,否則就會報錯。

03、作為參數傳遞

在下例中,有一個無參的構造方法,裏面調用了 print() 方法,參數只有一個 this 關鍵字。

public class ThisTest {
    public ThisTest() {
        print(this);
    }

    private void print(ThisTest thisTest) {
        System.out.println("print " +thisTest);
    }

    public static void main(String[] args) {
        ThisTest test = new ThisTest();
        System.out.println("main " + test);
    }
}

來打印看一下結果:

print com.cmower.baeldung.this1.ThisTest@573fd745
main com.cmower.baeldung.this1.ThisTest@573fd745

從結果中可以看得出來,this 就是我們在 main() 方法中使用 new 關鍵字創建的 ThisTest 對象。

04、鏈式調用

學過 JavaScript,或者 jQuery 的讀者可能對鏈式調用比較熟悉,類似於 a.b().c().d(),彷彿能無窮無盡調用下去。

在 Java 中,對應的專有名詞叫 Builder 模式,來看一個示例。

public class Writer {
    private int age;
    private String name;
    private String bookName;

    public Writer(WriterBuilder builder) {
        this.age = builder.age;
        this.name = builder.name;
        this.bookName = builder.bookName;
    }

    public static class WriterBuilder {
        public String bookName;
        private int age;
        private String name;

        public WriterBuilder(int age, String name) {
            this.age = age;
            this.name = name;
        }

        public WriterBuilder writeBook(String bookName) {
            this.bookName = bookName;
            return this;
        }

        public Writer build() {
            return new Writer(this);
        }
    }
}

Writer 類有三個成員變量,分別是 age、name 和 bookName,還有它們仨對應的一個構造方法,參數是一個內部靜態類 WriterBuilder。

內部類 WriterBuilder 也有三個成員變量,和 Writer 類一致,不同的是,WriterBuilder 類的構造方法裏面只有 age 和 name 賦值了,另外一個成員變量 bookName 通過單獨的方法 writeBook() 來賦值,注意,該方法的返回類型是 WriterBuilder,最後使用 return 返回了 this 關鍵字。

最後的 build() 方法用來創建一個 Writer 對象,參數為 this 關鍵字,也就是當前的 WriterBuilder 對象。

這時候,創建 Writer 對象就可以通過鏈式調用的方式。

Writer writer = new Writer.WriterBuilder(18,"沉默王二")
                .writeBook("《Web全棧開發進階之路》")
                .build();

05、在內部類中訪問外部類對象

說實話,自從 Java 8 的函數式編程出現后,就很少用到 this 在內部類中訪問外部類對象了。來看一個示例:

public class ThisInnerTest {
    private String name;

    class InnerClass {
        public InnerClass() {
            ThisInnerTest thisInnerTest = ThisInnerTest.this;
            String outerName = thisInnerTest.name;
        }
    }
}

在內部類 InnerClass 的構造方法中,通過外部類.this 可以獲取到外部類對象,然後就可以使用外部類的成員變量了,比如說 name。

十三、super 關鍵字

簡而言之,super 關鍵字就是用來訪問父類的。

先來看父類:

public class SuperBase {
    String message = "父類";

    public SuperBase(String message) {
        this.message = message;
    }

    public SuperBase() {
    }

    public void printMessage() {
        System.out.println(message);
    }
}

再來看子類:

public class SuperSub extends SuperBase {
    String message = "子類";

    public SuperSub(String message) {
        super(message);
    }

    public SuperSub() {
        super.printMessage();
        printMessage();
    }

    public void getParentMessage() {
        System.out.println(super.message);
    }

    public void printMessage() {
        System.out.println(message);
    }
}

1)super 關鍵字可用於訪問父類的構造方法

你看,子類可以通過 super(message) 來調用父類的構造方法。現在來新建一個 SuperSub 對象,看看輸出結果是什麼:

SuperSub superSub = new SuperSub("子類的message");

new 關鍵字在調用構造方法創建子類對象的時候,會通過 super 關鍵字初始化父類的 message,所以此此時父類的 message 會輸出“子類的message”。

2)super 關鍵字可以訪問父類的變量

上述例子中的 SuperSub 類中就有,getParentMessage() 通過 super.message 方法父類的同名成員變量 message。

3)當方法發生重寫時,super 關鍵字可以訪問父類的同名方法

上述例子中的 SuperSub 類中就有,無參的構造方法 SuperSub() 中就使用 super.printMessage() 調用了父類的同名方法。

十四、重寫和重載

先來看一段重寫的代碼吧。

class LaoWang{
    public void write() {
        System.out.println("老王寫了一本《基督山伯爵》");
    }
}
public class XiaoWang extends LaoWang {
    @Override
    public void write() {
        System.out.println("小王寫了一本《茶花女》");
    }
}

重寫的兩個方法名相同,方法參數的個數也相同;不過一個方法在父類中,另外一個在子類中。就好像父類 LaoWang 有一個 write() 方法(無參),方法體是寫一本《基督山伯爵》;子類 XiaoWang 重寫了父類的 write() 方法(無參),但方法體是寫一本《茶花女》。

來寫一段測試代碼。

public class OverridingTest {
    public static void main(String[] args) {
        LaoWang wang = new XiaoWang();
        wang.write();
    }
}

大家猜結果是什麼?

小王寫了一本《茶花女》

在上面的代碼中,們聲明了一個類型為 LaoWang 的變量 wang。在編譯期間,編譯器會檢查 LaoWang 類是否包含了 write() 方法,發現 LaoWang 類有,於是編譯通過。在運行期間,new 了一個 XiaoWang 對象,並將其賦值給 wang,此時 Java 虛擬機知道 wang 引用的是 XiaoWang 對象,所以調用的是子類 XiaoWang 中的 write() 方法而不是父類 LaoWang 中的 write() 方法,因此輸出結果為“小王寫了一本《茶花女》”。

再來看一段重載的代碼吧。

class LaoWang{
    public void read() {
        System.out.println("老王讀了一本《Web全棧開發進階之路》");
    }

    public void read(String bookname) {
        System.out.println("老王讀了一本《" + bookname + "》");
    }
}

重載的兩個方法名相同,但方法參數的個數不同,另外也不涉及到繼承,兩個方法在同一個類中。就好像類 LaoWang 有兩個方法,名字都是 read(),但一個有參數(書名),另外一個沒有(只能讀寫死的一本書)。

來寫一段測試代碼。

public class OverloadingTest {
    public static void main(String[] args) {
        LaoWang wang = new LaoWang();
        wang.read();
        wang.read("金");
    }
}

這結果就不用猜了。變量 wang 的類型為 LaoWang,wang.read() 調用的是無參的 read() 方法,因此先輸出“老王讀了一本《Web全棧開發進階之路》”;wang.read("金") 調用的是有參的 read(bookname) 方法,因此後輸出“老王讀了一本《》”。在編譯期間,編譯器就知道這兩個 read() 方法時不同的,因為它們的方法簽名(=方法名稱+方法參數)不同。

簡單的來總結一下:

1)編譯器無法決定調用哪個重寫的方法,因為只從變量的類型上是無法做出判斷的,要在運行時才能決定;但編譯器可以明確地知道該調用哪個重載的方法,因為引用類型是確定的,參數個數決定了該調用哪個方法。

2)多態針對的是重寫,而不是重載。

哎,後悔啊,早年我要是能把這道面試題吃透的話,也不用被老馬刁難了。吟一首詩感慨一下人生吧。

青青園中葵,朝露待日晞。
陽春布德澤,萬物生光輝。
常恐秋節至,焜黃華恭弘=叶 恭弘衰。
百川東到海,何時復西歸?
少壯不努力,老大徒傷悲

另外,我想要告訴大家的是,重寫(Override)和重載(Overload)是 Java 中兩個非常重要的概念,新手經常會被它們倆迷惑,因為它們倆的英文名字太像了,中文翻譯也只差一個字。難,太難了。

十五、static 關鍵字

先來個提綱挈領(唉呀媽呀,成語區博主上線了)吧:

static 關鍵字可用於變量、方法、代碼塊和內部類,表示某個特定的成員只屬於某個類本身,而不是該類的某個對象。

01、靜態變量

靜態變量也叫類變量,它屬於一個類,而不是這個類的對象。

public class Writer {
    private String name;
    private int age;
    public static int countOfWriters;

    public Writer(String name, int age) {
        this.name = name;
        this.age = age;
        countOfWriters++;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

其中,countOfWriters 被稱為靜態變量,它有別於 name 和 age 這兩個成員變量,因為它前面多了一個修飾符 static

這意味着無論這個類被初始化多少次,靜態變量的值都會在所有類的對象中共享。

Writer w1 = new Writer("沉默王二",18);
Writer w2 = new Writer("沉默王三",16);

System.out.println(Writer.countOfWriters);

按照上面的邏輯,你應該能推理得出,countOfWriters 的值此時應該為 2 而不是 1。從內存的角度來看,靜態變量將會存儲在 Java 虛擬機中一個名叫“Metaspace”(元空間,Java 8 之後)的特定池中。

靜態變量和成員變量有着很大的不同,成員變量的值屬於某個對象,不同的對象之間,值是不共享的;但靜態變量不是的,它可以用來統計對象的數量,因為它是共享的。就像上面例子中的 countOfWriters,創建一個對象的時候,它的值為 1,創建兩個對象的時候,它的值就為 2。

簡單小結一下:

1)由於靜態變量屬於一個類,所以不要通過對象引用來訪問,而應該直接通過類名來訪問;

2)不需要初始化類就可以訪問靜態變量。

public class WriterDemo {
    public static void main(String[] args) {
        System.out.println(Writer.countOfWriters); // 輸出 0
    }
}

02、靜態方法

靜態方法也叫類方法,它和靜態變量類似,屬於一個類,而不是這個類的對象。

public static void setCountOfWriters(int countOfWriters) {
    Writer.countOfWriters = countOfWriters;
}

setCountOfWriters() 就是一個靜態方法,它由 static 關鍵字修飾。

如果你用過 java.lang.Math 類或者 Apache 的一些工具類(比如說 StringUtils)的話,對靜態方法一定不會感動陌生。

Math 類的幾乎所有方法都是靜態的,可以直接通過類名來調用,不需要創建類的對象。

簡單小結一下:

1)Java 中的靜態方法在編譯時解析,因為靜態方法不能被重寫(方法重寫發生在運行時階段,為了多態)。

2)抽象方法不能是靜態的。

3)靜態方法不能使用 this 和 super 關鍵字。

4)成員方法可以直接訪問其他成員方法和成員變量。

5)成員方法也可以直接方法靜態方法和靜態變量。

6)靜態方法可以訪問所有其他靜態方法和靜態變量。

7)靜態方法無法直接訪問成員方法和成員變量。

03、靜態代碼塊

靜態代碼塊可以用來初始化靜態變量,儘管靜態方法也可以在聲明的時候直接初始化,但有些時候,我們需要多行代碼來完成初始化。

public class StaticBlockDemo {
    public static List<String> writes = new ArrayList<>();

    static {
        writes.add("沉默王二");
        writes.add("沉默王三");
        writes.add("沉默王四");

        System.out.println("第一塊");
    }

    static {
        writes.add("沉默王五");
        writes.add("沉默王六");

        System.out.println("第二塊");
    }
}

writes 是一個靜態的 ArrayList,所以不太可能在聲明的時候完成初始化,因此需要在靜態代碼塊中完成初始化。

簡單小結一下:

1)一個類可以有多個靜態代碼塊。

2)靜態代碼塊的解析和執行順序和它在類中的位置保持一致。為了驗證這個結論,可以在 StaticBlockDemo 類中加入空的 main 方法,執行完的結果如下所示:

第一塊
第二塊

04、靜態內部類

Java 允許我們在一個類中聲明一個內部類,它提供了一種令人信服的方式,允許我們只在一個地方使用一些變量,使代碼更具有條理性和可讀性。

常見的內部類有四種,成員內部類、局部內部類、匿名內部類和靜態內部類,限於篇幅原因,前三種不在我們本次文章的討論範圍,以後有機會再細說。

public class Singleton {
    private Singleton() {}

    private static class SingletonHolder {
        public static final Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

以上這段代碼是不是特別熟悉,對,這就是創建單例的一種方式,第一次加載 Singleton 類時並不會初始化 instance,只有第一次調用 getInstance() 方法時 Java 虛擬機才開始加載 SingletonHolder 並初始化 instance,這樣不僅能確保線程安全也能保證 Singleton 類的唯一性。不過,創建單例更優雅的一種方式是使用枚舉。

簡單小結一下:

1)靜態內部類不能訪問外部類的所有成員變量。

2)靜態內部類可以訪問外部類的所有靜態變量,包括私有靜態變量。

3)外部類不能聲明為 static。

十六、Java 枚舉

開門見山地說吧,enum(枚舉)是 Java 1.5 時引入的關鍵字,它表示一種特殊類型的類,默認繼承自 java.lang.Enum。

為了證明這一點,我們來新建一個枚舉 PlayerType:

public enum PlayerType {
    TENNIS,
    FOOTBALL,
    BASKETBALL
}

兩個關鍵字帶一個類名,還有大括號,以及三個大寫的單詞,但沒看到繼承 Enum 類啊?別著急,心急吃不了熱豆腐啊。使用 JAD 查看一下反編譯后的字節碼,就一清二楚了。

public final class PlayerType extends Enum
{

    public static PlayerType[] values()
    {
        return (PlayerType[])$VALUES.clone();
    }

    public static PlayerType valueOf(String name)
    
{
        return (PlayerType)Enum.valueOf(com/cmower/baeldung/enum1/PlayerType, name);
    }

    private PlayerType(String s, int i)
    
{
        super(s, i);
    }

    public static final PlayerType TENNIS;
    public static final PlayerType FOOTBALL;
    public static final PlayerType BASKETBALL;
    private static final PlayerType $VALUES[];

    static 
    {
        TENNIS = new PlayerType("TENNIS"0);
        FOOTBALL = new PlayerType("FOOTBALL"1);
        BASKETBALL = new PlayerType("BASKETBALL"2);
        $VALUES = (new PlayerType[] {
            TENNIS, FOOTBALL, BASKETBALL
        });
    }
}

看到沒?PlayerType 類是 final 的,並且繼承自 Enum 類。這些工作我們程序員沒做,編譯器幫我們悄悄地做了。此外,它還附帶幾個有用靜態方法,比如說 values()valueOf(String name)

01、內部枚舉

好的,小夥伴們應該已經清楚枚舉長什麼樣子了吧?既然枚舉是一種特殊的類,那它其實是可以定義在一個類的內部的,這樣它的作用域就可以限定於這個外部類中使用。

public class Player {
    private PlayerType type;
    public enum PlayerType {
        TENNIS,
        FOOTBALL,
        BASKETBALL
    }

    public boolean isBasketballPlayer() {
      return getType() == PlayerType.BASKETBALL;
    }

    public PlayerType getType() {
        return type;
    }

    public void setType(PlayerType type) {
        this.type = type;
    }
}

PlayerType 就相當於 Player 的內部類,isBasketballPlayer() 方法用來判斷運動員是否是一個籃球運動員。

由於枚舉是 final 的,可以確保在 Java 虛擬機中僅有一個常量對象(可以參照反編譯后的靜態代碼塊「static 關鍵字帶大括號的那部分代碼」),所以我們可以很安全地使用“==”運算符來比較兩個枚舉是否相等,參照 isBasketballPlayer() 方法。

那為什麼不使用 equals() 方法判斷呢?

if(player.getType().equals(Player.PlayerType.BASKETBALL)){};
if(player.getType() == Player.PlayerType.BASKETBALL){};

“==”運算符比較的時候,如果兩個對象都為 null,並不會發生 NullPointerException,而 equals() 方法則會。

另外, “==”運算符會在編譯時進行檢查,如果兩側的類型不匹配,會提示錯誤,而 equals() 方法則不會。

02、枚舉可用於 switch 語句

這個我在之前的一篇我去的文章中詳細地說明過了,感興趣的小夥伴可以點擊鏈接跳轉過去看一下。

switch (playerType) {
        case TENNIS:
            return "網球運動員費德勒";
        case FOOTBALL:
            return "足球運動員C羅";
        case BASKETBALL:
            return "籃球運動員詹姆斯";
        case UNKNOWN:
            throw new IllegalArgumentException("未知");
        default:
            throw new IllegalArgumentException(
                    "運動員類型: " + playerType);

    }

03、枚舉可以有構造方法

如果枚舉中需要包含更多信息的話,可以為其添加一些字段,比如下面示例中的 name,此時需要為枚舉添加一個帶參的構造方法,這樣就可以在定義枚舉時添加對應的名稱了。

public enum PlayerType {
    TENNIS("網球"),
    FOOTBALL("足球"),
    BASKETBALL("籃球");

    private String name;

    PlayerType(String name) {
        this.name = name;
    }
}

04、EnumSet

EnumSet 是一個專門針對枚舉類型的 Set 接口的實現類,它是處理枚舉類型數據的一把利器,非常高效(內部實現是位向量,我也搞不懂)。

因為 EnumSet 是一個抽象類,所以創建 EnumSet 時不能使用 new 關鍵字。不過,EnumSet 提供了很多有用的靜態工廠方法:

下面的示例中使用 noneOf() 創建了一個空的 PlayerType 的 EnumSet;使用 allOf() 創建了一個包含所有 PlayerType 的 EnumSet。

public class EnumSetTest {
    public enum PlayerType {
        TENNIS,
        FOOTBALL,
        BASKETBALL
    }

    public static void main(String[] args) {
        EnumSet<PlayerType> enumSetNone = EnumSet.noneOf(PlayerType.class);
        System.out.println(enumSetNone);

        EnumSet<PlayerType> enumSetAll = EnumSet.allOf(PlayerType.class);
        System.out.println(enumSetAll);
    }
}

程序輸出結果如下所示:

[]
[TENNIS, FOOTBALL, BASKETBALL]

有了 EnumSet 后,就可以使用 Set 的一些方法了:

05、EnumMap

EnumMap 是一個專門針對枚舉類型的 Map 接口的實現類,它可以將枚舉常量作為鍵來使用。EnumMap 的效率比 HashMap 還要高,可以直接通過數組下標(枚舉的 ordinal 值)訪問到元素。

和 EnumSet 不同,EnumMap 不是一個抽象類,所以創建 EnumMap 時可以使用 new 關鍵字:

EnumMap<PlayerType, String> enumMap = new EnumMap<>(PlayerType.class);

有了 EnumMap 對象后就可以使用 Map 的一些方法了:

和 HashMap 的使用方法大致相同,來看下面的例子:

EnumMap<PlayerType, String> enumMap = new EnumMap<>(PlayerType.class);
enumMap.put(PlayerType.BASKETBALL,"籃球運動員");
enumMap.put(PlayerType.FOOTBALL,"足球運動員");
enumMap.put(PlayerType.TENNIS,"網球運動員");
System.out.println(enumMap);

System.out.println(enumMap.get(PlayerType.BASKETBALL));
System.out.println(enumMap.containsKey(PlayerType.BASKETBALL));
System.out.println(enumMap.remove(PlayerType.BASKETBALL));

程序輸出結果如下所示:

{TENNIS=網球運動員, FOOTBALL=足球運動員, BASKETBALL=籃球運動員}
籃球運動員
true
籃球運動員

06、單例

通常情況下,實現一個單例並非易事,不信,來看下面這段代碼

public class Singleton {  
    private volatile static Singleton singleton; 
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {
        synchronized (Singleton.class) { 
        if (singleton == null) {  
            singleton = new Singleton(); 
        }  
        }  
    }  
    return singleton;  
    }  
}

但枚舉的出現,讓代碼量減少到極致:

public enum EasySingleton{
    INSTANCE;
}

完事了,真的超級短,有沒有?枚舉默認實現了 Serializable 接口,因此 Java 虛擬機可以保證該類為單例,這與傳統的實現方式不大相同。傳統方式中,我們必須確保單例在反序列化期間不能創建任何新實例。

07、枚舉可與數據庫交互

我們可以配合 Mybatis 將數據庫字段轉換為枚舉類型。現在假設有一個數據庫字段 check_type 的類型如下:

`check_type` int(1) DEFAULT NULL COMMENT '檢查類型(1:未通過、2:通過)',

它對應的枚舉類型為 CheckType,代碼如下:

public enum CheckType {
    NO_PASS(0"未通過"), PASS(1"通過");
    private int key;

    private String text;

    private CheckType(int key, String text) {
        this.key = key;
        this.text = text;
    }

    public int getKey() {
        return key;
    }

    public String getText() {
        return text;
    }

    private static HashMap<Integer,CheckType> map = new HashMap<Integer,CheckType>();
    static {
        for(CheckType d : CheckType.values()){
            map.put(d.key, d);
        }
    }

    public static CheckType parse(Integer index) {
        if(map.containsKey(index)){
            return map.get(index);
        }
        return null;
    }
}

1)CheckType 添加了構造方法,還有兩個字段,key 為 int 型,text 為 String 型。

2)CheckType 中有一個public static CheckType parse(Integer index)方法,可將一個 Integer 通過 key 的匹配轉化為枚舉類型。

那麼現在,我們可以在 Mybatis 的配置文件中使用 typeHandler 將數據庫字段轉化為枚舉類型。

<resultMap id="CheckLog" type="com.entity.CheckLog">
  <id property="id" column="id"/>
  <result property="checkType" column="check_type" typeHandler="com.CheckTypeHandler"></result>
</resultMap>

其中 checkType 字段對應的類如下:

public class CheckLog implements Serializable {

    private String id;
    private CheckType checkType;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public CheckType getCheckType() {
        return checkType;
    }

    public void setCheckType(CheckType checkType) {
        this.checkType = checkType;
    }
}

CheckTypeHandler 轉換器的類源碼如下:

public class CheckTypeHandler extends BaseTypeHandler<CheckType{

    @Override
    public CheckType getNullableResult(ResultSet rs, String index) throws SQLException {
        return CheckType.parse(rs.getInt(index));
    }

    @Override
    public CheckType getNullableResult(ResultSet rs, int index) throws SQLException {
        return CheckType.parse(rs.getInt(index));
    }

    @Override
    public CheckType getNullableResult(CallableStatement cs, int index) throws SQLException {
        return CheckType.parse(cs.getInt(index));
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int index, CheckType val, JdbcType arg3) throws SQLException {
        ps.setInt(index, val.getKey());
    }
}

CheckTypeHandler 的核心功能就是調用 CheckType 枚舉類的 parse() 方法對數據庫字段進行轉換。

恕我直言,我覺得小夥伴們肯定會用 Java 枚舉了,如果還不會,就過來砍我!

十七、final 關鍵字

儘管繼承可以讓我們重用現有代碼,但有時處於某些原因,我們確實需要對可擴展性進行限制,final 關鍵字可以幫助我們做到這一點。

01、final 類

如果一個類使用了 final 關鍵字修飾,那麼它就無法被繼承。如果小夥伴們細心觀察的話,Java 就有不少 final 類,比如說最常見的 String 類。

public final class String
    implements java.io.SerializableComparable<String>, CharSequence,
               ConstableConstantDesc 
{}

為什麼 String 類要設計成 final 的呢?原因大致有以下三個:

  • 為了實現字符串常量池
  • 為了線程安全
  • 為了 HashCode 的不可變性

更詳細的原因,可以查看我之前寫的一篇文章。

任何嘗試從 final 類繼承的行為將會引發編譯錯誤,為了驗證這一點,我們來看下面這個例子,Writer 類是 final 的。

public final class Writer {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

嘗試去繼承它,編譯器會提示以下錯誤,Writer 類是 final 的,無法繼承。

不過,類是 final 的,並不意味着該類的對象是不可變的。

Writer writer = new Writer();
writer.setName("沉默王二");
System.out.println(writer.getName()); // 沉默王二

Writer 的 name 字段的默認值是 null,但可以通過 settter 方法將其更改為“沉默王二”。也就是說,如果一個類只是 final 的,那麼它並不是不可變的全部條件。

如果,你想了解不可變類的全部真相,請查看我之前寫的文章這次要說不明白immutable類,我就怎麼地。突然發現,寫系列文章真的妙啊,很多相關性的概念全部涉及到了。我真服了自己了。

把一個類設計成 final 的,有其安全方面的考慮,但不應該故意為之,因為把一個類定義成 final 的,意味着它沒辦法繼承,假如這個類的一些方法存在一些問題的話,我們就無法通過重寫的方式去修復它。

02、final 方法

被 final 修飾的方法不能被重寫。如果我們在設計一個類的時候,認為某些方法不應該被重寫,就應該把它設計成 final 的。

Thread 類就是一個例子,它本身不是 final 的,這意味着我們可以擴展它,但它的 isAlive() 方法是 final 的:

public class Thread implements Runnable {
    public final native boolean isAlive();
}

需要注意的是,該方法是一個本地(native)方法,用於確認線程是否處於活躍狀態。而本地方法是由操作系統決定的,因此重寫該方法並不容易實現。

Actor 類有一個 final 方法 show()

public class Actor {
    public final void show() {

    }
}

當我們想要重寫該方法的話,就會出現編譯錯誤:

如果一個類中的某些方法要被其他方法調用,則應考慮事被調用的方法稱為 final 方法,否則,重寫該方法會影響到調用方法的使用。

一個類是 final 的,和一個類不是 final,但它所有的方法都是 final 的,考慮一下,它們之間有什麼區別?

我能想到的一點,就是前者不能被繼承,也就是說方法無法被重寫;後者呢,可以被繼承,然後追加一些非 final 的方法。沒毛病吧?看把我聰明的。

03、final 變量

被 final 修飾的變量無法重新賦值。換句話說,final 變量一旦初始化,就無法更改。之前被一個小夥伴問過,什麼是 effective final,什麼是 final,這一點,我在之前的文章也有闡述過,所以這裏再貼一下地址:

http://www.itwanger.com/java/2020/02/14/java-final-effectively.html

1)final 修飾的基本數據類型

來聲明一個 final 修飾的 int 類型的變量:

final int age = 18;

嘗試將它修改為 30,結果編譯器生氣了:

2)final 修飾的引用類型

現在有一個普通的類 Pig,它有一個字段 name:

public class Pig {
   private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

在測試類中聲明一個 final 修飾的 Pig 對象:

 final Pig pig = new Pig();

如果嘗試將 pig 重新賦值的話,編譯器同樣會生氣:

但我們仍然可以去修改 Pig 的字段值:

final Pig pig = new Pig();
pig.setName("特立獨行");
System.out.println(pig.getName()); // 特立獨行

3)final 修飾的字段

final 修飾的字段可以分為兩種,一種是 static 的,另外一種是沒有 static 的,就像下面這樣:

public class Pig {
   private final int age = 1;
   public static final double PRICE = 36.5;
}

非 static 的 final 字段必須有一個默認值,否則編譯器將會提醒沒有初始化:

static 的 final 字段也叫常量,它的名字應該為大寫,可以在聲明的時候初始化,也可以通過 static [代碼塊初始化]()。

4) final 修飾的參數

final 關鍵字還可以修飾參數,它意味着參數在方法體內不能被再修改:

public class ArgFinalTest {
    public void arg(final int age) {
    }

    public void arg1(final String name) {
    }
}

如果嘗試去修改它的話,編譯器會提示以下錯誤:

。。。。。。

後續還會繼續更新,但有些小夥伴可能就忍不住了,這份小白手冊有沒有 PDF 版可以白嫖啊,那必須得有啊,直接「沉默王二」公眾號後台回復「小白」就可以了,不要手軟,覺得不錯的,請多多分享——贈人玫瑰,手有餘香哦。

沒關注的話,掃描上面的二維碼就可以了,然後回復「小白」。

我是沉默王二,一枚有顏值卻靠才華苟且的程序員。關注即可提升學習效率,別忘了三連啊,點贊、收藏、留言,我不挑,嘻嘻

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心