Caffine Cache 在 SpringBoot 中的使用
點擊關(guān)注公眾號,Java干貨及時送達??

前面剛說到Guava Cache,他的優(yōu)點是封裝了get,put操作;提供線程安全的緩存操作;提供過期策略;提供回收策略;緩存監(jiān)控。當(dāng)緩存的數(shù)據(jù)超過最大值時,使用LRU算法替換。這一篇我們將要談到一個新的本地緩存框架:Caffeine Cache。它也是站在巨人的肩膀上-Guava Cache,借著他的思想優(yōu)化了算法發(fā)展而來。
本篇博文主要介紹Caffine Cache 的使用方式,以及Caffine Cache在SpringBoot中的使用。
|?Caffine Cache 在算法上的優(yōu)點-W-TinyLFU
FIFO:先進先出,在這種淘汰算法中,先進入緩存的會先被淘汰,會導(dǎo)致命中率很低。 LRU:最近最少使用算法,每次訪問數(shù)據(jù)都會將其放在我們的隊首,如果需要淘汰數(shù)據(jù),就只需要淘汰隊尾即可。仍然有個問題,如果有個數(shù)據(jù)在 1 分鐘訪問了 1000次,再后 1 分鐘沒有訪問這個數(shù)據(jù),但是有其他的數(shù)據(jù)訪問,就導(dǎo)致了我們這個熱點數(shù)據(jù)被淘汰。 LFU:最近最少頻率使用,利用額外的空間記錄每個數(shù)據(jù)的使用頻率,然后選出頻率最低進行淘汰。這樣就避免了 LRU 不能處理時間段的問題。
當(dāng)數(shù)據(jù)的訪問模式不隨時間變化的時候,LFU的策略能夠帶來最佳的緩存命中率。然而LFU有兩個缺點: 首先,它需要給每個記錄項維護頻率信息,每次訪問都需要更新,這是個巨大的開銷; 其次,如果數(shù)據(jù)訪問模式隨時間有變,LFU的頻率信息無法隨之變化,因此早先頻繁訪問的記錄可能會占據(jù)緩存,而后期訪問較多的記錄則無法被命中。 因此,大多數(shù)的緩存設(shè)計都是基于LRU或者其變種來進行的。相比之下,LRU并不需要維護昂貴的緩存記錄元信息,同時也能夠反應(yīng)隨時間變化的數(shù)據(jù)訪問模式。然而,在許多負(fù)載之下,LRU依然需要更多的空間才能做到跟LFU一致的緩存命中率。因此,一個“現(xiàn)代”的緩存,應(yīng)當(dāng)能夠綜合兩者的長處。

<dependency>
????<groupId>com.github.ben-manes.caffeinegroupId>
????<artifactId>caffeineartifactId>
????<version>2.6.2version>
dependency>
緩存填充策略
Caffeine Cache提供了三種緩存填充策略:手動、同步加載和異步加載。
手動加載
在每次get key的時候指定一個同步的函數(shù),如果key不存在就調(diào)用這個函數(shù)生成一個值。
/**
?????*?手動加載
?????*?@param?key
?????*?@return
?????*/
public?Object?manulOperator(String?key)?{
????Cache?cache?=?Caffeine.newBuilder()
????????.expireAfterWrite(1,?TimeUnit.SECONDS)
????????.expireAfterAccess(1,?TimeUnit.SECONDS)
????????.maximumSize(10)
????????.build();
????//如果一個key不存在,那么會進入指定的函數(shù)生成value
????Object?value?=?cache.get(key,?t?->?setValue(key).apply(key));
????cache.put("hello",value);
????//判斷是否存在如果不存返回null
????Object?ifPresent?=?cache.getIfPresent(key);
????//移除一個key
????cache.invalidate(key);
????return?value;
}
public?Function?setValue(String?key) {
????return?t?->?key?+?"value";
}
同步加載
構(gòu)造Cache時候,build方法傳入一個CacheLoader實現(xiàn)類。實現(xiàn)load方法,通過key加載value。
/**
?????*?同步加載
?????*?@param?key
?????*?@return
?????*/
public?Object?syncOperator(String?key){
????LoadingCache?cache?=?Caffeine.newBuilder()
????????.maximumSize(100)
????????.expireAfterWrite(1,?TimeUnit.MINUTES)
????????.build(k?->?setValue(key).apply(key));
????return?cache.get(key);
}
public?Function?setValue(String?key) {
????return?t?->?key?+?"value";
}
異步加載
AsyncLoadingCache是繼承自LoadingCache類的,異步加載使用Executor去調(diào)用方法并返回一個CompletableFuture。異步加載緩存使用了響應(yīng)式編程模型。
如果要以同步方式調(diào)用時,應(yīng)提供CacheLoader。要以異步表示時,應(yīng)該提供一個AsyncCacheLoader,并返回一個CompletableFuture。
?/**
?????*?異步加載
?????*
?????*?@param?key
?????*?@return
?????*/
public?Object?asyncOperator(String?key){
????AsyncLoadingCache?cache?=?Caffeine.newBuilder()
????????.maximumSize(100)
????????.expireAfterWrite(1,?TimeUnit.MINUTES)
????????.buildAsync(k?->?setAsyncValue(key).get());
????return?cache.get(key);
}
public?CompletableFuture{
????return?CompletableFuture.supplyAsync(()?->?{
????????return?key?+?"value";
????});
}
回收策略
Caffeine提供了3種回收策略:基于大小回收,基于時間回收,基于引用回收。
基于大小的過期方式
基于大小的回收策略有兩種方式:一種是基于緩存大小,一種是基于權(quán)重。
//?根據(jù)緩存的計數(shù)進行驅(qū)逐
LoadingCache?cache?=?Caffeine.newBuilder()
????.maximumSize(10000)
????.build(key?->?function(key));
//?根據(jù)緩存的權(quán)重來進行驅(qū)逐(權(quán)重只是用于確定緩存大小,不會用于決定該緩存是否被驅(qū)逐)
LoadingCache?cache1?=?Caffeine.newBuilder()
????.maximumWeight(10000)
????.weigher(key?->?function1(key))
????.build(key?->?function(key));
maximumWeight與maximumSize不可以同時使用。
基于時間的過期方式
//?基于固定的到期策略進行退出
LoadingCache?cache?=?Caffeine.newBuilder()
????.expireAfterAccess(5,?TimeUnit.MINUTES)
????.build(key?->?function(key));
LoadingCache?cache1?=?Caffeine.newBuilder()
????.expireAfterWrite(10,?TimeUnit.MINUTES)
????.build(key?->?function(key));
//?基于不同的到期策略進行退出
LoadingCache?cache2?=?Caffeine.newBuilder()
????.expireAfter(new?Expiry()?{
????????@Override
????????public?long?expireAfterCreate(String?key,?Object?value,?long?currentTime)?{
????????????return?TimeUnit.SECONDS.toNanos(seconds);
????????}
????????@Override
????????public?long?expireAfterUpdate(@Nonnull?String?s,?@Nonnull?Object?o,?long?l,?long?l1)?{
????????????return?0;
????????}
????????@Override
????????public?long?expireAfterRead(@Nonnull?String?s,?@Nonnull?Object?o,?long?l,?long?l1)?{
????????????return?0;
????????}
????}).build(key?->?function(key));
Caffeine提供了三種定時驅(qū)逐策略:
expireAfterAccess(long, TimeUnit):在最后一次訪問或者寫入后開始計時,在指定的時間后過期。假如一直有請求訪問該key,那么這個緩存將一直不會過期。expireAfterWrite(long, TimeUnit): 在最后一次寫入緩存后開始計時,在指定的時間后過期。expireAfter(Expiry): 自定義策略,過期時間由Expiry實現(xiàn)獨自計算。緩存的刪除策略使用的是惰性刪除和定時刪除。這兩個刪除策略的時間復(fù)雜度都是O(1)。
基于引用的過期方式
Java中四種引用類型
| 引用類型 | 被垃圾回收時間 | 用途 | 生存時間 |
|---|---|---|---|
| 強引用 Strong Reference | 從來不會 | 對象的一般狀態(tài) | JVM停止運行時終止 |
| 軟引用 Soft Reference | 在內(nèi)存不足時 | 對象緩存 | 內(nèi)存不足時終止 |
| 弱引用 Weak Reference | 在垃圾回收時 | 對象緩存 | gc運行后終止 |
| 虛引用 Phantom Reference | 從來不會 | 可以用虛引用來跟蹤對象被垃圾回收器回收的活動,當(dāng)一個虛引用關(guān)聯(lián)的對象被垃圾收集器回收之前會收到一條系統(tǒng)通知 | JVM停止運行時終止 |
//?當(dāng)key和value都沒有引用時驅(qū)逐緩存
LoadingCache?cache?=?Caffeine.newBuilder()
????.weakKeys()
????.weakValues()
????.build(key?->?function(key));
//?當(dāng)垃圾收集器需要釋放內(nèi)存時驅(qū)逐
LoadingCache?cache1?=?Caffeine.newBuilder()
????.softValues()
????.build(key?->?function(key));
注意:AsyncLoadingCache不支持弱引用和軟引用。
Caffeine.weakKeys():使用弱引用存儲key。如果沒有其他地方對該key有強引用,那么該緩存就會被垃圾回收器回收。由于垃圾回收器只依賴于身份(identity)相等,因此這會導(dǎo)致整個緩存使用身份 (==) 相等來比較 key,而不是使用 equals()。
Caffeine.weakValues() :使用弱引用存儲value。如果沒有其他地方對該value有強引用,那么該緩存就會被垃圾回收器回收。由于垃圾回收器只依賴于身份(identity)相等,因此這會導(dǎo)致整個緩存使用身份 (==) 相等來比較 key,而不是使用 equals()。
Caffeine.softValues() :使用軟引用存儲value。當(dāng)內(nèi)存滿了過后,軟引用的對象以將使用最近最少使用(least-recently-used ) 的方式進行垃圾回收。由于使用軟引用是需要等到內(nèi)存滿了才進行回收,所以我們通常建議給緩存配置一個使用內(nèi)存的最大值。softValues() 將使用身份相等(identity) (==) 而不是equals() 來比較值。
Caffeine.weakValues()和Caffeine.softValues()不可以一起使用。
移除事件監(jiān)聽
Cache?cache?=?Caffeine.newBuilder()
????.removalListener((String?key,?Object?value,?RemovalCause?cause)?->
?????????????????????System.out.printf("Key?%s?was?removed?(%s)%n",?key,?cause))
????.build();
寫入外部存儲
CacheWriter 方法可以將緩存中所有的數(shù)據(jù)寫入到第三方。
LoadingCache?cache2?=?Caffeine.newBuilder()
????.writer(new?CacheWriter()?{
????????@Override?public?void?write(String?key,?Object?value)?{
????????????//?寫入到外部存儲
????????}
????????@Override?public?void?delete(String?key,?Object?value,?RemovalCause?cause)?{
????????????//?刪除外部存儲
????????}
????})
????.build(key?->?function(key));
如果你有多級緩存的情況下,這個方法還是很實用。
注意:CacheWriter不能與弱鍵或AsyncLoadingCache一起使用。
統(tǒng)計
與Guava Cache的統(tǒng)計一樣。
Cache?cache?=?Caffeine.newBuilder()
????.maximumSize(10_000)
????.recordStats()
????.build();
通過使用Caffeine.recordStats(), 可以轉(zhuǎn)化成一個統(tǒng)計的集合. 通過 Cache.stats() 返回一個CacheStats。CacheStats提供以下統(tǒng)計方法:
hitRate():?返回緩存命中率
evictionCount():?緩存回收數(shù)量
averageLoadPenalty():?加載新值的平均時間|?SpringBoot 中默認(rèn)Cache-Caffine Cache
SpringBoot 1.x版本中的默認(rèn)本地cache是Guava Cache。在2.x(Spring Boot 2.0(spring 5)?)版本中已經(jīng)用Caffine Cache取代了Guava Cache。畢竟有了更優(yōu)的緩存淘汰策略。
下面我們來說在SpringBoot2.x版本中如何使用cache。
引入依賴
<dependency>
????<groupId>org.springframework.bootgroupId>
????<artifactId>spring-boot-starter-cacheartifactId>
dependency>
<dependency>
????<groupId>com.github.ben-manes.caffeinegroupId>
????<artifactId>caffeineartifactId>
????<version>2.6.2version>
dependency>
添加注解開啟緩存支持
添加@EnableCaching注解:
@SpringBootApplication
@EnableCaching
public?class?SingleDatabaseApplication?{
????public?static?void?main(String[]?args)?{
????????SpringApplication.run(SingleDatabaseApplication.class,?args);
????}
}
配置文件的方式注入相關(guān)參數(shù)
properties文件
spring.cache.cache-names=cache1
spring.cache.caffeine.spec=initialCapacity=50,maximumSize=500,expireAfterWrite=10s
或Yaml文件
spring:
??cache:
????type:?caffeine
????cache-names:
????-?userCache
????caffeine:
??????spec:?maximumSize=1024,refreshAfterWrite=60s
如果使用refreshAfterWrite配置,必須指定一個CacheLoader.不用該配置則無需這個bean,如上所述,該CacheLoader將關(guān)聯(lián)被該緩存管理器管理的所有緩存,所以必須定義為CacheLoader
import?com.github.benmanes.caffeine.cache.CacheLoader;
import?org.springframework.context.annotation.Bean;
import?org.springframework.context.annotation.Configuration;
/**
?*?@author:?rickiyang
?*?@date:?2019/6/15
?*?@description:
?*/
@Configuration
public?class?CacheConfig?{
????/**
?????*?相當(dāng)于在構(gòu)建LoadingCache對象的時候?build()方法中指定過期之后的加載策略方法
?????*?必須要指定這個Bean,refreshAfterWrite=60s屬性才生效
?????*?@return
?????*/
????@Bean
????public?CacheLoader?cacheLoader()? {
????????CacheLoader?cacheLoader?=?new?CacheLoader()?{
????????????@Override
????????????public?Object?load(String?key)?throws?Exception?{
????????????????return?null;
????????????}
????????????//?重寫這個方法將oldValue值返回回去,進而刷新緩存
????????????@Override
????????????public?Object?reload(String?key,?Object?oldValue)?throws?Exception?{
????????????????return?oldValue;
????????????}
????????};
????????return?cacheLoader;
????}
}
initialCapacity=[integer]:?初始的緩存空間大小
maximumSize=[long]:?緩存的最大條數(shù)
maximumWeight=[long]:?緩存的最大權(quán)重
expireAfterAccess=[duration]:?最后一次寫入或訪問后經(jīng)過固定時間過期
expireAfterWrite=[duration]:?最后一次寫入后經(jīng)過固定時間過期
refreshAfterWrite=[duration]:?創(chuàng)建緩存或者最近一次更新緩存后經(jīng)過固定的時間間隔,刷新緩存
weakKeys:?打開key的弱引用
weakValues:打開value的弱引用
softValues:打開value的軟引用
recordStats:開發(fā)統(tǒng)計功能
注意:
expireAfterWrite和expireAfterAccess同時存在時,以expireAfterWrite為準(zhǔn)。
maximumSize和maximumWeight不可以同時使用
weakValues和softValues不可以同時使用
需要說明的是,使用配置文件的方式來進行緩存項配置,一般情況能滿足使用需求,但是靈活性不是很高,如果我們有很多緩存項的情況下寫起來會導(dǎo)致配置文件很長。所以一般情況下你也可以選擇使用bean的方式來初始化Cache實例。
下面的演示使用bean的方式來注入:
package?com.rickiyang.learn.cache;
import?com.github.benmanes.caffeine.cache.CacheLoader;
import?com.github.benmanes.caffeine.cache.Caffeine;
import?org.apache.commons.compress.utils.Lists;
import?org.springframework.cache.CacheManager;
import?org.springframework.cache.caffeine.CaffeineCache;
import?org.springframework.cache.support.SimpleCacheManager;
import?org.springframework.context.annotation.Bean;
import?org.springframework.context.annotation.Configuration;
import?org.springframework.context.annotation.Primary;
import?java.util.ArrayList;
import?java.util.List;
import?java.util.concurrent.TimeUnit;
/**
?*?@author:?rickiyang
?*?@date:?2019/6/15
?*?@description:
?*/
@Configuration
public?class?CacheConfig?{
????/**
?????*?創(chuàng)建基于Caffeine的Cache?Manager
?????*?初始化一些key存入
?????*?@return
?????*/
????@Bean
????@Primary
????public?CacheManager?caffeineCacheManager()?{
????????SimpleCacheManager?cacheManager?=?new?SimpleCacheManager();
????????ArrayList?caches?=?Lists.newArrayList();
????????List?list?=?setCacheBean();
????????for(CacheBean?cacheBean?:?list){
????????????caches.add(new?CaffeineCache(cacheBean.getKey(),
????????????????????Caffeine.newBuilder().recordStats()
????????????????????????????.expireAfterWrite(cacheBean.getTtl(),?TimeUnit.SECONDS)
????????????????????????????.maximumSize(cacheBean.getMaximumSize())
????????????????????????????.build()));
????????}
????????cacheManager.setCaches(caches);
????????return?cacheManager;
????}
????/**
?????*?初始化一些緩存的?key
?????*?@return
?????*/
????private?List?setCacheBean() {
????????List?list?=?Lists.newArrayList();
????????CacheBean?userCache?=?new?CacheBean();
????????userCache.setKey("userCache");
????????userCache.setTtl(60);
????????userCache.setMaximumSize(10000);
????????CacheBean?deptCache?=?new?CacheBean();
????????deptCache.setKey("userCache");
????????deptCache.setTtl(60);
????????deptCache.setMaximumSize(10000);
????????list.add(userCache);
????????list.add(deptCache);
????????return?list;
????}
????class?CacheBean?{
????????private?String?key;
????????private?long?ttl;
????????private?long?maximumSize;
????????public?String?getKey()?{
????????????return?key;
????????}
????????public?void?setKey(String?key)?{
????????????this.key?=?key;
????????}
????????public?long?getTtl()?{
????????????return?ttl;
????????}
????????public?void?setTtl(long?ttl)?{
????????????this.ttl?=?ttl;
????????}
????????public?long?getMaximumSize()?{
????????????return?maximumSize;
????????}
????????public?void?setMaximumSize(long?maximumSize)?{
????????????this.maximumSize?=?maximumSize;
????????}
????}
}
創(chuàng)建了一個SimpleCacheManager作為Cache的管理對象,然后初始化了兩個Cache對象,分別存儲user,dept類型的緩存。當(dāng)然構(gòu)建Cache的參數(shù)設(shè)置我寫的比較簡單,你在使用的時候酌情根據(jù)需要配置參數(shù)。
使用注解來對 cache 增刪改查
我們可以使用spring提供的?@Cacheable、@CachePut、@CacheEvict等注解來方便的使用caffeine緩存。
如果使用了多個cahce,比如redis、caffeine等,必須指定某一個CacheManage為@primary,在@Cacheable注解中沒指定 cacheManager 則使用標(biāo)記為primary的那個。
cache方面的注解主要有以下5個:
@Cacheable 觸發(fā)緩存入口(這里一般放在創(chuàng)建和獲取的方法上, @Cacheable注解會先查詢是否已經(jīng)有緩存,有會使用緩存,沒有則會執(zhí)行方法并緩存)@CacheEvict 觸發(fā)緩存的eviction(用于刪除的方法上) @CachePut 更新緩存且不影響方法執(zhí)行(用于修改的方法上,該注解下的方法始終會被執(zhí)行) @Caching 將多個緩存組合在一個方法上(該注解可以允許一個方法同時設(shè)置多個注解) @CacheConfig 在類級別設(shè)置一些緩存相關(guān)的共同配置(與其它緩存配合使用)
說一下@Cacheable?和?@CachePut的區(qū)別:
@Cacheable:它的注解的方法是否被執(zhí)行取決于Cacheable中的條件,方法很多時候都可能不被執(zhí)行。
@CachePut:這個注解不會影響方法的執(zhí)行,也就是說無論它配置的條件是什么,方法都會被執(zhí)行,更多的時候是被用到修改上。
簡要說一下Cacheable類中各個方法的使用:
public?@interface?Cacheable?{
????/**
?????*?要使用的cache的名字
?????*/
????@AliasFor("cacheNames")
????String[]?value()?default?{};
????/**
?????*?同value(),決定要使用那個/些緩存
?????*/
????@AliasFor("value")
????String[]?cacheNames()?default?{};
????/**
?????*?使用SpEL表達式來設(shè)定緩存的key,如果不設(shè)置默認(rèn)方法上所有參數(shù)都會作為key的一部分
?????*/
????String?key()?default?"";
????/**
?????*?用來生成key,與key()不可以共用
?????*/
????String?keyGenerator()?default?"";
????/**
?????*?設(shè)定要使用的cacheManager,必須先設(shè)置好cacheManager的bean,這是使用該bean的名字
?????*/
????String?cacheManager()?default?"";
????/**
?????*?使用cacheResolver來設(shè)定使用的緩存,用法同cacheManager,但是與cacheManager不可以同時使用
?????*/
????String?cacheResolver()?default?"";
????/**
?????*?使用SpEL表達式設(shè)定出發(fā)緩存的條件,在方法執(zhí)行前生效
?????*/
????String?condition()?default?"";
????/**
?????*?使用SpEL設(shè)置出發(fā)緩存的條件,這里是方法執(zhí)行完生效,所以條件中可以有方法執(zhí)行后的value
?????*/
????String?unless()?default?"";
????/**
?????*?用于同步的,在緩存失效(過期不存在等各種原因)的時候,如果多個線程同時訪問被標(biāo)注的方法
?????*?則只允許一個線程通過去執(zhí)行方法
?????*/
????boolean?sync()?default?false;
}
package?com.rickiyang.learn.cache;
import?com.rickiyang.learn.entity.User;
import?org.springframework.cache.annotation.CacheEvict;
import?org.springframework.cache.annotation.CachePut;
import?org.springframework.cache.annotation.Cacheable;
import?org.springframework.stereotype.Service;
/**
?*?@author:?rickiyang
?*?@date:?2019/6/15
?*?@description:?本地cache
?*/
@Service
public?class?UserCacheService?{
????/**
?????*?查找
?????*?先查緩存,如果查不到,會查數(shù)據(jù)庫并存入緩存
?????*?@param?id
?????*/
????@Cacheable(value?=?"userCache",?key?=?"#id",?sync?=?true)
????public?void?getUser(long?id){
????????//查找數(shù)據(jù)庫
????}
????/**
?????*?更新/保存
?????*?@param?user
?????*/
????@CachePut(value?=?"userCache",?key?=?"#user.id")
????public?void?saveUser(User?user){
????????//todo?保存數(shù)據(jù)庫
????}
????/**
?????*?刪除
?????*?@param?user
?????*/
????@CacheEvict(value?=?"userCache",key?=?"#user.id")
????public?void?delUser(User?user){
????????//todo?保存數(shù)據(jù)庫
????}
}
如果你不想使用注解的方式去操作緩存,也可以直接使用SimpleCacheManager獲取緩存的key進而進行操作。
注意到上面的key使用了spEL 表達式。Spring Cache提供了一些供我們使用的SpEL上下文數(shù)據(jù),下表直接摘自Spring官方文檔:
| 名稱 | 位置 | 描述 | 示例 |
|---|---|---|---|
| methodName | root對象 | 當(dāng)前被調(diào)用的方法名 | #root.methodname |
| method | root對象 | 當(dāng)前被調(diào)用的方法 | #root.method.name |
| target | root對象 | 當(dāng)前被調(diào)用的目標(biāo)對象實例 | #root.target |
| targetClass | root對象 | 當(dāng)前被調(diào)用的目標(biāo)對象的類 | #root.targetClass |
| args | root對象 | 當(dāng)前被調(diào)用的方法的參數(shù)列表 | #root.args[0] |
| caches | root對象 | 當(dāng)前方法調(diào)用使用的緩存列表 | #root.caches[0].name |
| Argument Name | 執(zhí)行上下文 | 當(dāng)前被調(diào)用的方法的參數(shù),如findArtisan(Artisan artisan),可以通過#artsian.id獲得參數(shù) | #artsian.id |
| result | 執(zhí)行上下文 | 方法執(zhí)行后的返回值(僅當(dāng)方法執(zhí)行后的判斷有效,如 unless cacheEvict的beforeInvocation=false) | #result |
注意:
1.當(dāng)我們要使用root對象的屬性作為key時我們也可以將“#root”省略,因為Spring默認(rèn)使用的就是root對象的屬性。如
@Cacheable(key?=?"targetClass?+?methodName?+#p0")
2.使用方法參數(shù)時我們可以直接使用“#參數(shù)名”或者“#p參數(shù)index”。如:
@Cacheable(value="userCache",?key="#id")
@Cacheable(value="userCache",?key="#p0")
SpEL提供了多種運算符
| 類型 | 運算符 |
|---|---|
| 關(guān)系 | <,>,<=,>=,==,!=,lt,gt,le,ge,eq,ne |
| 算術(shù) | +,- ,* ,/,%,^ |
| 邏輯 | &&,||,!,and,or,not,between,instanceof |
| 條件 | ?: (ternary),?: (elvis) |
| 正則表達式 | matches |
| 其他類型 | ?.,?[…],![…],^[…],$[…] |
1.?著名開源項目作者刪庫跑路,神秘Bug影響超20000個項目
最近面試BAT,整理一份面試資料《Java面試BATJ通關(guān)手冊》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。
獲取方式:點“在看”,關(guān)注公眾號并回復(fù)?Java?領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
PS:因公眾號平臺更改了推送規(guī)則,如果不想錯過內(nèi)容,記得讀完點一下“在看”,加個“星標(biāo)”,這樣每次新文章推送才會第一時間出現(xiàn)在你的訂閱列表里。
點“在看”支持小哈呀,謝謝啦??!

