Java面试题-Java核心基础-第九天(泛型)
目录
一、泛型的理解
泛型在jdk5中开始有的,泛型其实就是将类型进行参数话,使得类型在编译时就确定了,这种类型参数可以用在类、接口、方法上面
泛型的初衷是为了安全和方便的 安全是类型转换安全 方便是指可以不需要类型转换了
它的这种安全体现在如果说编译的时候能够检测出来问题,最好,就不要拖到运行的时候再来出错
有句话正好贴合这里:越早出错,代价越好
例子:
Object s= new String();
Integer s1 = (Interger)s;这个编译的时候不会出错,但是实际运行会出现ClassCastException类型转换异常。
如果说使用了泛型的话,那么是那种类型就得是哪种类型
List<String> list = new ArrayList();
实现安全就体现在添加元素上面,其次如果取元素,那么就只能使用正确的类型进行接收,如果类型不一致接收都接收不了,不能强转成非法的类型
二、泛型的作用
其实作用大的来说就两点:安全 + 方便
安全:避免错误的强转,比如说现在定义一个泛型类型为String类型的集合,那么从中取出来元素就不能强转成其他类型,否则就会报错。如果说它让你强转的话,那么在运行时会出错
参考上面理解中的例子
方便:其实就是不需要手动的强转了,自动的为我们强转...这里的例子,比如说方法返回值接收
当然除了这两点主要的,还有其他的一些好处:
1. 如果说使用了包装类型那么就根本就不需要拆箱与装箱
其实原因就在于指定了上面类型就会自动的返回什么类型 而不会像方法那么传递一个Integer返回一个int造成需要拆箱了
2. 提高了代码的重用性,需要什么类型我就指定好什么类型就行了。比如说一个求和的方法,我不再需要写多个重载的方法了,参数传递什么类型 结果就能是什么类型
三、泛型有哪些使用方式
泛型类
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
典型应用:用在接口统一结果返回对象
泛型方法
public static < E > void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
泛型方法不等于泛型类中的那个使用泛型属性的方法。那个有局限性:
我如果想要换一种类型,是不是就得重新创建另外一种类型的对象,不方便
典型应用:集合工具类中的排序方法,是需要对各种类型的集合都能排序的
如果是没有泛型方法,那么每次对一个新的类型的集合进行排序,就需要创建一个新的工具类对象
不够灵活
当然还有许多地方:
很多时候我们是需要直接就能得到对应的对象,而不想让得到一个Object类对象,我们再去进行强转。
可以使用泛型方法,参数就写Class<T> clazz
然后在方法中强转 成T类型
public static <T> T copyBean(Object source,Class<T> clazz){
T result = null;
try {
result = clazz.newInstance();
BeanUtils.copyProperties(source,result);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return result;
}
public static <T> List<T> copyBean(List<?> list,Class<T> clazz){
return list.stream()
.map(o->copyBean(o,clazz))
.collect(Collectors.toList());
}
public <T> T getData(TypeReference<T> typeReference) {
Object data = get("data"); //默认是map
String jsonString = JSON.toJSONString(data);
T t = JSON.parseObject(jsonString, typeReference);
return t;
}
public <R,ID> R queryShopWithCacheThrough(String cachePrefix, ID id, Class<R> type, Function<ID,R> dbFallBack,Long time, TimeUnit timeUnit){
//1. 从redis中查询缓存
String key = cachePrefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
//2. 判断是否存在
if(StrUtil.isNotBlank(json)){
//3. 若存在,则直接返回
R r = JSONUtil.toBean(json, type);
return r;
}
if(json!=null){
// 查到空数据,直接返回错误
return null;
}
//4. 若不存在,则查询数据库
R r = dbFallBack.apply(id);
//5. 不存在,返回错误
if (r == null) {
//缓存空数据
stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//6. 存在,存入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(r),time,timeUnit);
//7. 返回
return r;
}
泛型接口:
public interface Generator<T> {
public T method();
}
实现泛型接口,不指定类型:
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method() {
return null;
}
}
典型应用:
在MP中的mapper接口、service接口都使用到了泛型接口 :
MP中的mapper接口和service接口使用的就是上面那种 通过指定实体类的类型,从而确定是对哪张表进行操作,所以此时不能再传递参数的时候传递其他表对应的实体类了,以及wrapper也是
public interface CourseBaseMapper extends BaseMapper<CourseBase> {
}
public interface BaseMapper<T> extends Mapper<T> {
int insert(T entity);
}
service:
public interface CourseBaseService extends IService<CourseBase> {}
public interface IService<T> {
int DEFAULT_BATCH_SIZE = 1000;
default boolean save(T entity) {
return SqlHelper.retBool(this.getBaseMapper().insert(entity));
}
}
四、上限、下限通配符的使用
首先介绍? 这个代表所有 是所有 T 的根父类
比如说我可以这样 List<?> list = new ArrayList();
List<String> list1 = new ArrayList();
将list1赋值给list list = list1 这样不会报错
但是如果我是 List<Object> list = new ArrayList();
List<String> list1 = new ArrayList(); 那么就会报错。
小插一句:这里如果是 Person<T> p = new Person(); Student<T> s = new Student();
那么可以将 s 赋值给 p p = s 这里就是单纯的子父类了
原因在于相当于 如果这样可以的话 相当于集合中现在就只能添加String类型的元素了,相当于就是可添加的范围减少了,显然这是不合理的,另外还有就是你声明的类型是 Object而实际的类型是String 那我如果取元素,那么取出来的元素到底应该是String还是Object呢?其实就说不清了,存也是一样,我到底是存String还是存Object呢?
其实通配符的作用就来了,使用?就可以代替Object 就可以接收任意泛型类型
如果只是使用? 那么就是对泛型类型没有约束,什么类型都可以。但是如果你想要指定的范围,比如说我要求你给我的泛型类型必须是某一个类的父类或者是子类 ,那么就可以使用带范围的通配符。
<? extends Person>这个代表泛型类型只能是Person类型或者是其子类 也就是规定了上限
<? super Person>这个代表泛型类型只能是Person类型或者是Person的父类,规定了下限
那么方法参数的泛型类型就必须老实按规定来。
注意:
当集合类型的泛型定义为通配符的时候,是不能往里面添加元素的。只能存null 只能取元素
比如说:
List<?> list = new ArrayList(); list.add("avc") 这是会编译报错的。但是可以存null
这里看似应该是可以传递的,因为是?可以代表是Object的意思,但是为什么不能传,只能存null可能就是它这么规定的吧。
这个获取到集合中的元素,元素类型是Object类型
另外还有有范围的,比如说:
List<? extends Person> list = new ArrayList<>()
也只能存null,这个好理解,因为怕我存Person的父类对象进去了,为了安全起见,干脆就只能让你存null进去,这个因为无论怎样元素一定是Person类型或者是其子类类型,那么所以就完全使用Person类就可以接收
反过来如果是super,它怕你存进去Person的子类所以直接不让你存,就只能让你存null
这里因为不知道存进去的到底有多大,因此这个取出来直接使用根父类Object类接收
使用例子:
public static <T> List<T> copyBean(List<?> list,Class<T> clazz){
return list.stream()
.map(o->copyBean(o,clazz))
.collect(Collectors.toList());
}
因为这里我不确定到底是需要对什么类型的集合进行拷贝,但是这里又不能写死成Object,所以就只能使用?通配符了,当然也可以使用 T的方式,那么相当于就多了一个泛型参数,所以在前面定义的时候也需要多给一个,就是下面这样:
public static <T,O> List<T> copyBean(List<O> list,Class<T> clazz){
return list.stream()
.map(o->copyBean(o,clazz))
.collect(Collectors.toList());
}
五、泛型的原理
原理就是泛型擦除,其实泛型信息只会在编译时候保持,运行时就全没了,字节码中其实有泛型和没泛型的是一样的 如果没有设定范围 那么会被擦除成Object类型 如果给定了范围 那么擦除之后就是对应类型的 所以在编译的时候可以使用到编译器进行检查,因此这里不涉及到JVM,因此泛型是几乎不消耗性能的,所以使用泛型的另外一个作用就是:潜在的性能收益