结合模板设计模式讲讲我对泛型的一些看法

通常,注解,泛型,反射,在普通的开发中, 我们只是在用,但谈及怎么去实现,有些朋友可能还不太清楚,因为这些技术在程序开发的日常中,用不到。

但是这些技术又会给我们的开发带来很多的便利,所以我觉得应该了解一些。
今天就结合着模板设计模式来讲一下泛型在哪用,怎么用,以及哪些要注意的地方。

首先谈一谈我对模板模式的理解。

在一个系列的行为中,有一些是确定的,有一些是不明确的,我们把确定的行为定义在一个抽象类中,不确定的行为定义为抽象方法,由具体的子类去实现,这种不影响整个流程,但可以应对各种情况的方法就可以称之为模板模式。

比如说:我在前面文章提到的通用Adapter的设计,
由adapter的用法可分析到这样一个流程:
<ul>
<li>要有一组数据源,这个数据源可以确定是一个List但不知道具体的对象是什么</li>
<li>getItem()这个只要有了数据源,就可以得到,</li>
<li>getCount()只要有了数据源,就可以得到,</li>
<li>getView()中,要什么样的view不确定,要怎么样填数据不确定</li>
<li>但是如何复用convertView是可以确定的,这个复用又牵涉到viewholder的问题,要做成通用,</li>
<li>我们没有办法去知道使用者会定义一个什么样的viewholder,所以必须强加一个约束。</li>
</ul>

对于不明确的对象,我们可以用泛型来代表
对于不明确的行为,我们要用抽象方法来隔离。比如:要什么样的view不确定,要怎么样填数据不确定
对于不确定的viewHolder,我们应该定义一个父类的规则,让子类去遵守,并用泛型去代表变化量

所以代码就成了这样:

public abstract class LBaseAdapter<E, V extends LBaseAdapter.BaseViewHolder> extends BaseAdapter {

    private Context context;
    private List<E> dataSource = new ArrayList<>(); //初始化一个防止getCount()空指针

    public LBaseAdapter(Context context) {
        this.context = context;
    }

    public Context getContext() {
        return context;
    }

    //替换原有数据源
    public void setDataSource(List<E> dataSource) {
        setDataSource(dataSource,true);
    }

    //如果isClear==true,则替换原有数据源,否则加到数据源后面
    public void setDataSource(List<E> dataSource, boolean isClear) {
        if (isClear) this.dataSource.clear();
        this.dataSource = dataSource;
        notifyDataSetChanged();
    }

    //只加一个数据
    public void addData(E data) {
        this.dataSource.add(data);
        notifyDataSetChanged();
    }

    //通过下标移除一条数据
    public void removeData(int position) {
        this.dataSource.remove(position);
        notifyDataSetChanged();
    }

    //通过对象移除一条数据
    public void removeData(E data) {
        this.dataSource.remove(data);
        notifyDataSetChanged();
    }


    @Override
    public int getCount() {
        return this.dataSource.size();
    }


    @Override
    public E getItem(int position) {
        return this.dataSource.get(position);
    }


    @Override
    public long getItemId(int position) {
        return position;
    }


    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        V viewHolder = null;
        if (convertView == null) {
            viewHolder = createViewHolder(position, parent);
            if (viewHolder == null || viewHolder.getRootView() == null) {
                throw new NullPointerException("createViewHolder不能返回null或view为null的实例");
            }
            convertView = viewHolder.getRootView();
            convertView.setTag(viewHolder);
        }else{
            viewHolder = (V) convertView.getTag();
        }
        //给当前复用的holder一个正确的position
        viewHolder.setPosition(position);
        bindViewHolder(viewHolder,position,getItem(position));
        return viewHolder.getRootView();
    }

    protected abstract V createViewHolder(int position, ViewGroup parent);

    protected abstract void bindViewHolder(V holder,int position, E data);

    public static class BaseViewHolder {
        private View rootView;
        private SparseArray<View> viewCache = new SparseArray<>();
        private int position = -1;

        public View getRootView() {
            return rootView;
        }

        void setPosition(int position) {
            this.position = position;
        }

        public int getPosition() {
            return position;
        }

        public BaseViewHolder(View rootView) {
            this.rootView = rootView;
        }

        public <R> R getView(@IdRes int viewID) {
            View cachedView = viewCache.get(viewID);
            if(null == cachedView) {
                cachedView = rootView.findViewById(viewID);
                viewCache.put(viewID, cachedView);
            }
            return (R) cachedView;
        }
    }
}

所有的公用行为以及常用行为都抽到一个父类中,
利用泛型解决了不明确类型的问题,
得用抽象方法解决了不明确行为的问题

这样对于子类来说,它只用关心到自己的逻辑,也就是,需要的实体,要创建的view,以及实体要如何绑定数据,
代码就不帖过来了,因为这篇我要讲的是泛形。

在比如,在安卓开发中, 我们常常要请求接口以获取数据,且通常会定义一个公用的返回格式,举个例子:

{
   "success":true,
   "error":"",
   "data":{
     "xx":"xx
   }
}

{
   "success":true,
   "error":"",
   "data":[{
     "xx":"xx
   }]
}

对于单数据,data会返回一个json对象,对于一个列表数据,data会代表一个json数组,这种情况下,如果把返回json转成一个类实例,应该怎么写呢?不用泛型的话:

//单个对象情况:
public class UserLoginBean {

    public boolean success;
    public String error;
    public User data;
    
    
    public static class User{
        public String userName;
        public String userId;
        public String userNickName;
    }
}
//多个对象情况:
public class ArticleListBean {

    public boolean success;
    public String error;
    public List<Article> data;
    
    public static class Article{
        public String articleTitle;
        public String articleContent;
        public String articleCreateTime;
    }
}

这样写可以,但有这么几个问题

1.对于懒癌晚期的我,不能忍受写这么多重复代码。
2.如果Article有只获取一篇文章的情况,那么又要复制过去再重写一个ArticleBean?
3.如果项目二期,或者后期,经过开会商量返回值里面的error不好,要改个名字,或者加几个字段,
比如一次请求的总条数,那么很悲剧。你要改掉所有的bean.

针对这种情况:

public class BaseRet<T> {
    public boolean success;
    public String error;
    public T data;
}
        String json = "xxxx后台返回的json";
        BaseRet<User> ret = new Gson().fromJson(json, new TypeToken<BaseRet<User>>() {
        }.getType());
        if (ret != null) {
            if (ret.success) {
                User user = ret.data;
                // 填值
            } else {
                String error = ret.error;
                // Toast 显示error
            }
        }

        String json = "xxxx后台返回的json";
        BaseRet<List<User>> ret = new Gson().fromJson(json, new TypeToken<BaseRet<List<User>>>() {
        }.getType());
        if (ret != null) {
            if (ret.success) {
                List<User> user = ret.data;
                // 填值
            } else {
                String error = ret.error;
                // Toast 显示error
            }
        }
    }

我为了演示方便,没有写getters和setters,这种写法并不好,应该这样

public class BaseRet<T> {
    public boolean success;
    public String error;
    public T data;

    public boolean isSuccess() {
        return success;
    }
    public String getError() {
        return error;
    }
    public T getData() {
        return data;
    }
}

为什么只提供了getters,这是为了应对前面所说的,如果这个返回格式改了一个名字,我们可以这样应对,
比如:error改成msg

public class BaseRet<T> {
    public boolean success;
    public String msg;
    public T data;

    public boolean isSuccess() {
        return success;
    }

    public String getError() {
        return msg;
    }

    public T getData() {
        return data;
    }

}

如果你之前的代码都用的isSuccess(),getError(),则不需要更改。当然这也是getters存在的意义,如果不存在这种情况,直接用public的属性,不通过方法的活,效率会更好一点。
以上是一点经验之谈,也是简单的模板模式的应用,对于这种情况,变化的是data的类型,用泛型解决就可。

泛型分为声明和使用,两步,
比如一个方法,我们经常遇到TextView txt = (TextView)findViewById(R.id.txt):


    public static <R> R findView(Activity v, @IdRes int viewId) {
        return (R) v.findViewById(viewId);
    }

//用法
        TextView txt = findView(this, R.id.txt);

        ImageView img = findView(this, R.id.img);

findView方法中,声明了一个泛型<R> , 并指定返回值为R , 编译器会在编译时检测调用时要的类型,从而把R替换成具体的类型,所以
TextView txt = findView(this, R.id.txt);
这时声明的R就被替换成了TextView,最终的结果就成了
TextView txt = (TextView)activity.findViewById(viewId);

在举一个例子,
我要实现一个把一种数据转成另一种数据的方法,

    public static <R,T> List<R> map(List<T> param) {
//声明一个R做为返回值,一个T做为参数,
//但是到这里的时候,我进行不下去了,因为我什么都不知道,对我来说,T,R都是未知的,我也同样不知道要抽什么属性到一个数组里面去,但是我知道怎么去循环param,
             List<R> list = new ArrayList<R>();
             for(T entity : param) {
                    String result = 某种转换之后
                    list.add(result);
             }
             return list;
    }

有了不确定的行为了,就需要抽象来帮忙,这个具体的行为只有使用者结合他的业务才知道,所以我抽象一个接口:
这个接口在类上做了两个声明,一个R做为返回值的代表,一个T做为传入值的代表

    public interface Func<R,T> {
        public R apply(T t);
    }

于是可以:

    public static <R,T> List<R> mapToList(Func<R,T> func,List<T> t) {
        List<R> list = new ArrayList<>();
        for(T entity : t) {
            list.add(func.apply(entity));
        }
        return list;
    }
    

怎么用呢

        User user = new User("张三",22);
        User user1 = new User("李四",22);
        User user2 = new User("王五",22);
        User user3 = new User("赵六",22);
        List<User> asList = Arrays.asList(user,user1,user2,user3);

        List<String> names = mapToList(new Func<String, User>() {

            @Override
            public String apply(User t) {
                return t.name;
            }
        },asList);
        
        System.out.println(names);

//lambda
        names = mapToList(it->it.name, asList);
        System.out.println(names);
    
        

[张三, 李四, 王五, 赵六]

剩下的还有边界的问题,还是举adapter的那个例子吧,
我需要一个子类的ViewHolder,但是我又需要子类的ViewHolder能有一个我知道的规范,比如提供view的方法,这样我才能在getView里面setTag做常规的复用逻辑,这种情况下,我需要强制子类继承我定的规范,所以我写了一个BaseViewHolder,但是怎么做约束呢?

public abstract class LBaseAdapter<E, V extends LBaseAdapter.BaseViewHolder> extends BaseAdapter 

这里面写到了 V extends LBaseAdapter.BaseViewHolder , 这是泛型里面做限定的一种写法,意味着 V这个类型必须是BaseViewHolder的子类,如果使用者传了一个不相干的类型,则开发工具会报错。

泛型的另一种好处可能就是运行时类型检查和去除强转吧,这个不说了,用的很多:
List<String> list;
list.add(1) ; //会报错
String str = get(0);//正确
int object = get(0);会报错

java的泛型比如弱,而且简单,是一种基于jvm的伪泛型,所以能做的事情并不多,
但是如果结合了反射,会灵活一些,也麻烦些,这个在后面的文章中我会继续讲述。

如果这篇文章能给你带来新的认识,希望你能帮我顶起来,让更多的人看到
如果我的理解有误,或者用法不当,恳请指出

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容