忙碌中求生活-记录23岁生日

此时周六晚七点半,刚吃了一碗自己做的番茄鸡蛋面,将浸泡了很久的银耳原料装入了电饭煲内,静待熟时。回到自己的房间,窗外不断传来来往的车轱辘声,好像在告诉我他们很忙吧。看到书桌上的日历,还沉浸在六月份,我可能也很忙吧,都忘记将它带入七月份。

来杭州一年有余,从一名大三学生暑假实习,到大四学生实习,再到应届毕业生签订转正合同,从而成为一名正式的“社会人儿”。时间过得真的很快吧,一周周一下子就没有了。进入了七月份,明显感觉比之前更忙了,或者说是因为自己身份的转变,导致肩上的责任变得不太一样了吧。因为工作节奏的改变,有时候也变得有些麻木吧,日记有时候总会忘记写,写字这件事感觉也变得有些奢侈起来,日语学习的节奏好像变得慢了起来。但是时间的脚步并不因为你的忙碌而停下来。

我最近在读《时间简史》,讲述了从认知革命到农业革命再到人类文化的融合统一再到科学革命,作者用通俗的话语讲完了整个人类历史,真的很值得阅读。其中有一章节引发了我的思考,作者认为:农业革命是史上最大的骗局。因为在长达250万里里人类都靠狩猎或者采集果实为生,而到大约一万年前全然改变,从日升到日落,人类忙着对植物的培育,一心认为这样就能得到更多的水果、谷物和肉类,使得人类生活能够变得更加容易。然而事实上是人变得越来越辛苦。人类每次决定多做一点事情(比如用锄头来耕地,而不是直接将种子撒在地里面),我们认为这样没错,这样会使我们的收成更好一点,有了更好的收成,就不用更多地去担心荒年的问题了,不用挨饿了。工作努力一点,生活也能过得好一点。不过这都是理想的状态。

人们确实工作得更努力也更辛苦,但没想到大家的孩子也更多了,人口慢慢地增加了,所得到的食物也就变得少了,生存压力也就更大了,资源也开始变得稀缺,而引发各种低问题。可是为什么他们不赶快放弃农耕回到原始的采集社会?原因在于,所有的改变都是必须点滴积累,经过许多代,才能改变社会,等到那个时候,已经没有人记得过去的生活方式和现在有什么不一样了,也没得选了。采用了农耕生活,村落的人口从100人到了110人,难道会有10个人自愿挨饿,好让其他人回到过去的美好时光?但这已经无法回头。于是人类付出了更多,但得到的却变得没有以前那样多。

我们都各自为生活变得轻松而努力,但是事实上我们过得并不轻松。人的欲望永远也满足不了,今天拥有了这个,明天还想拥有更好的。现在,我们随手可以发送一条信息,传到地球另一方,而他立马能够回你。我们确实省下了很多时间和麻烦,但生活真的更轻松了么?我们以为省了时间,然而我们其实是把生活的步调调成了过去的10倍,于是我们整天忙忙碌碌、焦躁不安。

这是原书作者对农业革命是史上最大的骗局的论证吧,我再赞同不过了。每每与我的爸妈打电话,我都会有很大的感触,他们总是在忙碌,他们每天都在拼命的挣钱,可是日子真的好起来了么?生活真的轻松了么?不久前,在家族微信群里面,我妈拍了一张我爸的照片放在群里,我爸双手背后,露出一脸笑意,背景好像是他们住那里的一个普通的建筑吧。想表现出: 你看我和你妈在玩,我们多开心啊。 当我看到这张照片的时候,我心里五谷杂粮。

一方面是 我看到我爸的头发,愈发的白了,白了大多半了。记得上一次有这感触的时候,还是我大二的时候,那次我从学校回家,他到车站来接我,我坐他后面,我发现他的白头已经有很多很多了,那时我差点哭了出来。爸妈真的老了,可我还没有给他们带来好的生活,我曾告诉他们说:等我实习你们就回家吧,不要工作了,我能养活我自己,以后也能养活你们。可是他们并不,他们还是在工作,烈日下,每每打电话跟我说太阳是有多么多大温度是有多么高,我心里都很难受。可是他们总会说一句话:“这都是为了你以后更轻松一点”。此时我却不知道该说些什么。

另一方面是 我看着我妈拍的那张照片,真的很糊,不是我妈不会拍照,也不是她眼神不好,真的是她的手机像素不好。我的爸妈,辛苦了大半辈子,其实也有很多积蓄了,虽然没有大城市里的那样多,但在老家里也算是稍微有一些钱的,可是他们却舍不得给自己多花一分钱,能将就用则用。每次电话,我都给他们讲让他们对自己好一点,我说 你们到底挣钱是为了做什么?给他们讲了很多道理,可是他们也总是会一句:“这都是为了你以后更轻松一点”。而我也只能强忍着,因为我现在还没有足够的能力,心里暗暗发誓:我会让他们过上轻松地日子。

我其实一直都在思考一个问题:人到底活在这个世界上是为了什么?科学家无法解释,这是一个哲学问题,没有人知道正确的答案。我们只是沧海中的一粟,如果掀不起波浪,那么就好好感受海的浩瀚吧。忙忙碌碌,短视频以及直播的崛起,可能真的是因为都市生活节奏变得太快,于是都在夹缝中去寻找那一丝丝快感,其实可以做的很多吧,摘自网上“试着每天自己为自己做美味的饭菜,试着经常联络一下家人好友,试着拾起丢下很久的小说,试着用心养一颗植物,试着在空气清新的清晨去跑步,试着约一下自己暗恋已久的女孩…”。看看足球比赛,看看电影,多出去走走,再忙也不要忘记生活吧。

快九点了,删删写写,也不道该放一些什么在日志上,那就这样吧。
祝自己二十三岁生日快乐

记录两张此时自己二十三岁的照片,没出门没刮胡子没收拾,一张沧桑一张微笑,生活亦如此吧。

xxx.jpg
xxx.jpg 隐私保护

—-二零一九年六月十一

单例模式的设计

我们都知道单例模式很简单,大概是这样:

1
2
3
4
5
6
7
8
9
10
//单线程单例模式实现
public class Singleton {
private static Singleton instance=null;
public static Singleton getInstance() {
if(null==instance){
instance = new Singleton();
}
return instance;
}
}

但是呢,在多线程条件下getInstance()并不是一个原子操作。由于代码没有使用任何同步机制,因此该线程可能会出现线程交错的情形:在instance还是null的时候,如果两个线程同时执行到 if(null==instance)那么会创建两个实例,从而违背了初衷。于是通过简单加锁来解决这种问题:

1
2
3
4
5
6
7
8
9
10
11
12
//简单加锁实现单例模式
public class Singleton {
private static Singleton instance=null;
public static Singleton getInstance() {
synchronized (Singleton.class){//加入synchronized同步
if(null==instance){
instance = new Singleton();
}
return instance;
}
}
}

这种方式实现单例模式固然安全,但意味着每次调用 getInstance()都会申请锁,为了避免开销,我们想到了另一种办法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//基于双重检查锁定的错误单例模式实现
public class Singleton {
private static Singleton instance=null;
public static Singleton getInstance() {
if(null==instance){//先检查是否为null,再执行之上的代码
synchronized (Singleton.class){
if(null==instance){
instance = new Singleton();
}
}
}
return instance;
}
}

通过这种方法,虽然第一次检查对变量instance的访问没有加锁从而使竞态仍然可能存在,它似乎避免了锁的开销又保障了线程的安全。然后对 instance = new Singleton();进行伪代码独立子操作:

1
2
3
obj=allocate(Singleton.class);//1、分配对象所需的存储空间
invokeConstructor(obj);//2、初始化obj的引用对象
instance=obj;//3、将对象引用写入共享变量

由于重排序的规则,临界区内的操作可以再临界区内重排序,因此JIT编译器可能将上述子操作重排序为:1->3->2,即在初始化对象之前将对象引用写入实例变量instace。由于锁对有序性的保障是有条件的,而操作1读取intance变量的时候并没有加锁,因此重排序是对1操作是有影响的:该线程可能看到一个未初始化(或者为初始化完毕)的实例,即intance不为null。于是该线程直接就直接返回这个instance变量所引用的实例,而实例可能是未初始化完毕的,这就是可能导致程序出错。明白问题的原因之后,解决方法也不难想到了:只需将instance变量加入volatile修饰则可。于是代码变成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//基于双重检查锁定的正确单例模式实现
public class Singleton {
private static volatile Singleton instance=null;//加入volatile修饰
public static Singleton getInstance() {
if(null==instance){
synchronized (Singleton.class){
if(null==instance){
instance = new Singleton();
}
}
}
return instance;
}
}

到此为止,才正确实现安全的“单例模式”。

参考:《黄文海-Java多线程编程实战指南(核心篇)》

Android监听截屏事件之媒体读取的探索

最近做了一个需求:监听用户截屏,然后生成相关海报。
参考了Android 截屏事件监听的文章,大致思路是:a

1、利用ContentObserver用来监听指定Uri的所有资源变化,当媒体库中有相关图片新增的时候,则发送相关的通知。

2、得到回调的Uri后,借助ContentResolver在媒体数据库中查询最后一条数据

3、对数据做一些过滤。比如短时间重复截屏的情况以及其他App也插入了媒体文件等情况做处理。

不过有一些适配性的问题:

1、截屏后读取文件数据库后获取到件的绝对路径后,利用“screenshot”等关键字判断是否是截屏图片,并不能适配所有手机截屏的命名规则,以及其他应用同时间产生带有“screenshot”等关键词的文件也会有问题。

2、在某些型号手机中(现遇到Vivo)从数据库中读取的文件并不是获取到的最新的截屏文件,而且其他目录的文件,这里就有些难以理解了,所以今天取探究一下媒体数据库的读取。

其中ContentObserver如下代码所示:

` /**
 * 媒体内容观察者(观察媒体数据库的改变)
 */
private class MediaContentObserver extends ContentObserver {
    private Uri mContentUri;
    public MediaContentObserver(Uri contentUri, Handler handler) {
        super(handler);
        mContentUri = contentUri;
    }
    [@Override](https://my.oschina.net/u/1162528)
    public void onChange(boolean selfChange) {
        super.onChange(selfChange);
        handleMediaContentChange(mContentUri);
    }
}`

其中获取最后一次更新的媒体文件时的代码(为便于查看 删除了判空处理代码):

 /**
 * 处理媒体数据库的内容改变
 */
private void handleMediaContentChange(Uri contentUri) {
    Cursor cursor = null;
    /** 读取媒体数据库时需要读取的列 */
    private static final String[] MEDIA_PROJECTIONS =  {
        MediaStore.Images.ImageColumns.DATA,
        MediaStore.Images.ImageColumns.DATE_TAKEN };
    try {
        // 数据改变时查询数据库中最后加入的一条数据
        cursor = mContext.getContentResolver().query(
                contentUri,
                 MEDIA_PROJECTIONS,
                null,
                null,
                MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1"
        );
        // 获取各列的索引
        int dataIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
        int dateTakenIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN);

        // 获取行数据
        String data = cursor.getString(dataIndex);
        long dateTaken = cursor.getLong(dateTakenIndex);

        // 处理获取到的第一行数据
        handleMediaRowData(data, dateTaken);
}

这次的目的主要探究的是从数据库获取相关信息的过程

1、Android 的多媒体如何存储?

Android的多媒体文件主要存储在 /data/data/com.android.providers.media/databases 目录下,该目录下有连个db文件:

内部存储数据库文件:internal.db

存储卡数据库:external-XXXX.db

媒体文件的操作主要是围绕着这两个数据库来进行,这两个数据库的结构是一样的。

这两个数据库包含这些表:
album_art 、audio 、search 、album_info 、audio_genres、 searchhelpertitle、albums、 audio_genres_map、 thumbnails、
android_metadata、 audio_meta、 video、artist_info 、audio_playlists 、videothumbnails、artists 、audio_playlists_map、
artists_albums_map 、images

2、表的结构
对于Images表:主要存储images信息。表结构如下:

CREATE TABLE images (
_id INTEGER PRIMARY KEY, 
_data TEXT,
_size INTEGER,
_display_name TEXT,
mime_type TEXT,
title TEXT, 
date_added INTEGER, 
date_modified INTEGER,
description TEXT,
picasa_id TEXT,
isprivate INTEGER,
latitude DOUBLE, 
longitude DOUBLE, 
datetaken INTEGER, 
orientation INTEGER, 
mini_thumb_magic INTEGER, 
bucket_id TEXT, 
bucket_display_name TEXT );

`

各字段所表示意思,如图所示:

图片来自:Android MediaProvider数据库模式说明

所以在截屏监听数据的时候所读取的数据库返回值,分别为:

_data :图片据对路径

datetaken:取子EXIF照片拍摄事件,空的话为文件修改时间

private static final String[] MEDIA_PROJECTIONS =  {
      MediaStore.Images.ImageColumns.DATA,
      MediaStore.Images.ImageColumns.DATE_TAKEN };

在查询过程中构造的数据库代码为:

public final Cursor query (Uri uri, 
    String[] projection,
    String selection, 
    String[] selectionArgs, 
    String sortOrder)

`
其中对应的构造参数官方解释为:

uri The URI, using the content:// scheme, for the content to retrieve.

projection A list of which columns to return. Passing null will return all columns, which is inefficient.

selection A filter declaring which rows to return, formatted as an SQL WHERE clause (excluding the WHERE itself). Passing null will return all rows for the given URI.

selectionArgs You may include ?s in selection, which will be replaced by the values from selectionArgs, in the order that they appear in the selection. The values will be bound as Strings.

sortOrder How to order the rows, formatted as an SQL ORDER BY clause (excluding the ORDER BY itself). Passing null will use the default sort order, which may be unordered.


所以参数依次为:
所要查找的目标、所要的返回值、条件限制(类似sql中where)、匹配项、排序规则

所以这里的查询就显而易见了:获取最新图片数据库下data和datatoken列的数据

cursor = mContext.getContentResolver().query(
              contentUri,
              MEDIA_PROJECTIONS,
              null,
              null,
              MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1"
      );

然而…并不能解释vivo手机为什么查找出来不是最新截图的图片的问题

Android在子线程中创建Handler为什么会抛出异常?

复习一下消息机制,如下代码:

new Thread() {
        Handler handler = null;
        [@Override](https://my.oschina.net/u/1162528)
        public void run() {
            handler = new Handler();
        }
    }.start();

如果执行会抛出异常:

Can't create handler inside thread Thread.currentThread() that has not called Looper.prepare()

这是为什么呢?

我们进入Handler的构造方法

    public Handler(Callback callback, boolean async) {
    if (FIND_POTENTIAL_LEAKS) {
        final Class<? extends Handler> klass = getClass();
        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                (klass.getModifiers() & Modifier.STATIC) == 0) {
            Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                klass.getCanonicalName());
        }
    }

    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread " + Thread.currentThread()
                    + " that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

从上面的代码我们很清新的可以知道mLooper 为null,那么就会抛出这样的异常,那么mLooper 为什么会为空呢?这是因为在线程中的Looper还未被创建,所以在Looper.myLooper()中sThreadLocal.get()就会返回null。

我们知道 Handler的作用是处理消息,将消息传递给MessageQueue,而MessageQueue存在于Looper中,如果没有Looper那么就没有MessageQueue,所以创建Handler时,Looper不能够为空。

所以以上代码可以进行一个修改:

private void test() {
    new Thread() {
        Handler handler = null;
        [@Override](https://my.oschina.net/u/1162528)
        public void run() {
            Looper.prepare();
            handler = new Handler();
            Looper.loop();
        }
    }.start();
}

其中 Looper.prepare();为当前线程创建Looper并绑定在ThreadLocal中
Looper.loop();执行消息循环,这样子 Handler就能够正常工作了。

写在我即将毕业旅行前

忙碌的一天,又要到下班的时候了,一天好似很忙,其实回过头来仔细想想,一天好像并没有做太多的事情,然而确实是这样子度过一天了,就像我们的青春岁月,看样子并没有经历什么,但它就这样匆匆过去了,亦忧伤,亦憧憬。

还有两周就要回校参加毕业典礼了,向公司请了十天假,算上端午以及周末,大概有半个多月属于自己的日子吧,也算是大学最后一点还是以学生身份存在的时间了。早在几个月前我都在筹划这次出去游玩了,因为我想出去走走,想出去看看。纵观整个大学生涯,自己也算是去了不少地方了,屋子里车票、机票已经收纳了一沓了。看着每一张车票的起点与终点以及行程时间,还能想起那时候发生过的事,或许有些事还在记忆里面,或许可能存在我的日记里面,或许我什么也记不得了。

2015年8月15日 绵阳-上海:距离大学开学还有一个月了,父亲陪我度过了整个高三,待我拿到通知书后,我与父亲来到上海。父亲是一个节俭的人,买了绿皮车,将近两千多公里的路程,三十多个小时的行程,脚没有办法舒展开,时间长了别的难受。当看到那些没有座位的那些人要不停地给过路人让路的时候,我觉得自己也挺幸运的,至少我还有一个属于自己的位置,不需要为别人让路。在这煎熬的行程中,我并没有去抱怨,我只是一直在想,我以后一定要努力,一定不会让我和我父亲再受这样的遭遇。

2015年9月14日 上海-温州:马上就要大学开学了。离开了父母,将一个人去面对新的生活了,路过杭州的时候,车窗外一片开阔的平原,让我看到极具江南特色的景象:鳞次栉比的房屋伴着小河流,来来往往的车辆川流不息,很憧憬那样“小桥流水人家”的生活,这大概是对杭州的第一印象吧。

2016年1月22日 温州-成都:大学寒假第一次回家,运气很好,我和同学抢到了卧铺票,有了一个较舒服的乘车环境。与我通行的还有很多大学川籍同学,因此一路上也变得并不孤独,时而和他们“摆龙门阵”,累了就回床铺上面去休息一下,我记得我当时带了一本《浪潮之巅》。

2017年7月18日 温州-太原:第一次坐飞机,带着许多憧憬出发,但让人失望的是我错过了那一天的飞机,原本安排的好好的行程,一下子把我的计划打乱了。这时候我真的很迷茫,我不知道该去哪里,我也不知道该做什么,整个脑子是一片空白,在街上走走,最后回学校见到了精神支柱。然后去了太原平遥古城,喜欢太原老大爷那种街边悠然下着象棋的生活。

2017年7月10日 嘉兴-福州:又是一次暑假,我去嘉兴看望我爸,然后我爸他们提议去福州找我的大姑爷家玩,这一次我和我的父亲快吵了一架,因为我想让他买一张动车票,可父亲执着的只买绿皮车票,他觉得能节约钱,可是我觉得时间才是最重要的,那时候我没有一点收入,我说服不了他,绿皮车从晚上八点开到第二天早上八点,整个一晚我没有怎么睡,感慨太多,我也告诉我自己要努力。当时也写过一篇随笔:2017年710随笔 于嘉兴-福州列车

2017年9月02日 温州-杭州:这一次算是第一次以异地恋的身份去见了那时喜欢的人,一路忐忑。记得那时候带的是一本《小王子》,但是感觉没有看太懂,好像过了天真的年纪,已经看不太明白小王子的天真了。

2018年8月23日 杭州-太原: 或许真的是和太原有缘分,时隔一年再次去太原,这次我并没有错过,过了一年,变得成熟些了,这时候我已经在参加实习了。这次是代表整个学校唯一一只队伍进电子商务全国总决赛,压力与动力并存吧。

2019年2月14日 德阳-成都:这是最近一次从家出发去杭州在成都中转,在德阳站中因为高铁晚点,我遇到了一个女孩子,或许这是读大学以后认识的第一个非本大学的四川女孩子吧,在她回头的那一下,我感觉我好像心动了一下。或许是缘分,我和她是同一辆车,在德阳到成都只有短短半个小时的行车时间,居然车晚点将近两个小时,于是和她就聊啊,聊啊,从小学说到初中高中,从高中说到大学,再从大学说到实习。原来她也和我一样是大四的学生,原来我和她大学同学是初中高中同学,真的是缘分吧。第一次是多么的希望列车晚点的时间能够再长一点。只可惜,我本将心照明月,奈何明月照沟渠。

……

2019年6月10日 杭州-青岛、烟台、威海、大连? 或许是在被公司同事的鼓励下:现在有时间多出去玩玩吧,实习一天也没有多少钱,等以后正式工作了,有钱也没有时间了,趁现在,多出去走走吧。感谢芳姐姐对我的支助,让我有机会去计划这次旅行。

这一次,我是第一次一个人的旅行,我想在这炎热的夏天里,走出屋子,踏上行程。去坐一次轮渡、我还想再使用学生证享受一次学生特权、然后去看看海、去吹吹风、去看看更多的天空、去认识的人并说:“很高兴认识你”。也算是对整个大学青春岁月画上一个句号吧。
那接下来的一路上又会发生什么故事呢?

最后以王小波的《黄金时代》结尾吧:那一天我二十一岁,在我一生的黄金时代,我有好多奢望。我想爱,想吃,还想在一瞬间变成天上半明半暗的云,后来我才知道,生活就是个缓慢受锤的过程,人一天天老下去,奢望也一天天消逝,最后变得像挨了锤的牛一样。可是我过二十一岁生日时没有预见到这一点。我觉得自己会永远生猛下去,什么也锤不了我。

我还年轻,我还可以到处走走,我还有很多想要去憧憬的、盼望的……

–于2019.06.04 7:24 即将下班回家

95后,毕业半年,你过得好吗?

原帖:https://www.zhihu.com/question/307640916/answer/686748493
95后,毕业半年,你过得好吗?
亲爱的95后,毕业两年,你过得怎么样了?充实或是空虚,甜蜜还是苦涩,热爱生活还是生无可恋,期待写下你的故事。

96年 刚参加完学校的毕业答辩回来,离毕业也不远了吧。从去年暑假开始就在杭州实习了,最开始是做php后端,后来转到了安卓开发。实习了差不多快一年了,就要成“资深实习生”了。说实话我感觉我过得很充实吧,依然热爱生活。实习期间租房+伙食费+路费,如果不干别的什么事,一个月请假天数在两三天的话,每个月的生活是过得还算是比较舒服的,不太愁。实习之后就第一个月问家里要了一下房租和押金,以及驾校的学费。可以说完全独立了吧。拿到公司的offer是10几k,还是蛮期待拿毕业证转正的时候吧:
96年 刚参加完学校的毕业答辩回来,离毕业也不远了吧。从去年暑假开始就在杭州实习了,最开始是做php后端,后来转到了安卓开发。实习了差不多快一年了,就要成“资深实习生”了。说实话我感觉我过得很充实吧,依然热爱生活。实习期间租房+伙食费+路费,如果不干别的什么事,一个月请假天数在两三天的话,每个月的生活是过得还算是比较舒服的,不太愁。实习之后就第一个月问家里要了一下房租和押金,以及驾校的学费。可以说完全独立了吧。拿到公司的offer是10几k,还是蛮期待拿毕业证转正的时候吧。

在这里我好想讲讲近一年的实习生活,过得日子也算是充满酸甜苦辣吧。从刚开始从公司实习开始讲吧:当时的我好像什么都会,php也会、python、java、安卓啥都会一样,经过被阿里面试打击后,可以看此帖子:https://www.zhihu.com/question/268713348/answer/352195054

我稍微收敛了一点了吧,最后进入了杭州一家互联网公司,刚开始是干php的,干了一个多月一点,感觉并没有学到多少东西,做的业务比较多,因为我属于“实习”的状态吧,安排的任务也比较少,所以每天的日子过得也很“悠闲”,每天干完就回家了,大概下午六点多就走了……这前面的php实习期间算是伏笔吧。之后由于刚好公司内部有个安卓实习的位置,好像是没有招到合适的吧,最开始面试的时候说了我什么都会,所以有幸被调到了安卓组里面。我发现好像每天的需求做起来还是那么简单,所以每天依然走得特别早吧,还有点沾沾自喜的样子,然而回到家并没有继续学习相关知识,要不就和女朋友( broken up.)漫步钱塘江边,要么就是回到家里写写字看看其他类型的书,反正过得很舒服吧。

直到有一天我被“伤自尊”了,一度自我感觉良好的我被组内的同事叫去看一个问题,我看了很久也看不出来,因为很多Java基础我都有些快忘了吧,基础不太扎实,我对代码的深度理解也有问题,我只停留在用的阶段,我不懂其原理,我只会使用,我连他们的源码都没有看过。

后来被他鞭策道:你现在确实很厉害,比我当时刚出大学的时候厉害多了,但是我觉得我一点比你做的好,那就是我一直坚持在学习。你每天回去那么早没有学习,你在做什么?你现在对你自己的定位有些问题,现在你虽然觉得这些需求能做,但是你知道这些需求都是最简单的,工作难度都是最低的,我们为什么不把那些高难的任务给你?是因为你现在能力还不够,现在给你简单任务就是想让你多一些时间去学习,让你尽早能够跟上团队的步伐。实话给你说:你很菜!你真的很菜……你师傅可能不太好给你讲所以我才给你讲这些,如果你现在的状态,你永远只能做那些最简单的任务,可能连之后的校招offer都拿不到,就算侥幸8月份能拿到校招offer,也许你之后能侥幸转正,但是你不坚持学习的话迟早会被行业淘汰……

这位前辈的话一直在我脑海里面印象深刻,我是一个不服输的人,在被鞭策后,我真的不服气,我也不认输,当时他叫我周末会去看一看”EventBus”的源码,然后周一向他汇报一下看的结果。周末连续两天都在看其源码,说实话看起来真的很痛苦,因为自己以前都是直接用,不会去关心它内部的逻辑,但是依然坚持看了两天,做了很多笔记,把它内部实现逻辑也搞明白了。忐忑不安地等到了周一,我向他汇报,我给他讲述了内部原理以及源码解读,他给我提了几个问题,我答上了一半多一点,我正沾沾自喜时,他给我说:如果给你这次评价满分一百分的话,我最多给你打20分。

!!!!20分,我的天!我当时一下子整个人就不好了,甚至都想去反驳他了,可是他之后给我讲得东西,让我打消了这个念头,我确实菜。我确实认识到了自己的不足,对源代码的解读真的还不够,Java基础也不太扎实,包括我的师傅也这样认为,我是一个不服输的人,不服气。我向我师傅请教了,大概给自己定了一个短期的学习计划:先把Java基础过一遍,再过一遍Android基础过一把,再开始去理解安卓深度的东西。从此开始,我感觉我开始暴走了。

我每天都背一两本书回去学习,在地铁上有时候位置空我也会拿出来继续学习,偶尔还是会找女朋友去玩,但是当我11点钟回到家,我还是会拿出书来继续学习。然后第二天我会和我的师傅讲我昨天学了什么,然后讲出我的疑问。我的师傅真的是一个很耐心的人,他会给我仔细地讲解,包括平时的问题,如果我向组内请教问:1+1等于几? 他们会告诉我等于2。然而我的师傅会告诉我 加法口诀表,甚至是乘法口诀表。那时候我每天都不曾忘记学习,每天都会去研究,那一段时间我真的压力很大,经常性的失眠,心跳特别快,后面也去看过一次医生说是“心悸”。当然那也是在压力下人做出的一些极端反应,所幸的是我坚持了过来。八月中,HR把我叫了过去,我还以为她又要批判我了,因为最开始被团队的人给鞭策了之后,她也找过我谈话了:你如果还继续现在的状态,可能之后的校招offer都不会发给你。这一次不一样了,她给我讲了我转正后的待遇,什么期权还要公积金啥啥啥的,然后讲了工资多少多少,其实工资是有点超乎我预期的。她讲完之后,我真的笑了,真的开心了,我忍不住笑了起来。她问:开心吗?我一直点头。或许这是对努力之后最好的回报吧!那一刻真的,心里有太多说不出来,打心地的开心。

拿到校招offer我还是坚持学习吧,只是强度没有之前那样强了,但是依然坚持每天要学习。现在我觉得每天每周过得生活都特别的充实吧,我们不是996。公司9点上班,弹性打卡9个小时,也就是说早上9点钟打卡,下午6点钟就可以走了,但是大部分人并不是6点钟走。现在自己每天早上都会比较期盼去公司,因为每次做需求,我都并不把他当做我的“工作”在做,而我认为那是我的兴趣爱好。我喜欢敲代码的感觉,我喜欢那种自己去设计思路想法,最后打磨出来产品的过程。而我们做出的成果会是被几百万用户所使用,肩上也有一种责任感吧。然后也带来了成就感,比如公司的宣传片或者展示公告大牌上面印着app的应用界面图,会看到里面有一部分是我做得,我是多么的开心呀。一次室友的朋友过来了,我们聊着天,后来聊到它妈妈也在用我们做得app,它给我说它妈妈怎么怎么这个App,我说啊,这里就是我做的呀。哇!厉害咯。我对工作保持乐观态度,因为我可以向厉害的前辈们请教问题,向他们学习,我每天都感觉自己有收获,所以在公司让我感觉过得很开心,没有太大的压力,每天都在进步。

当然回到家了,我也有该做的事,我也有我的兴趣,大概每天7.30左右下班回家,回到家快8.30了。我也有自己追求,我在学日语,打算能在明年去一趟日本,感受一下不同的文化。日语学累了,我会继续学习相关专业的书籍,或者写写字,我比较喜欢写字,虽然没有很飘逸炫酷,但是我觉得写完之后看起来特别的舒服,这也算是一种成就感把,大概学到10.30收拾洗漱,大概11点左右上床了,我会继续看一些相关的书籍,比如最近在看《红楼梦》,厚厚的一本,渐渐地书签也跑到了中间的位置吧。

至于周末,我可能比较放松吧,周末我会学着去做做菜,不再想吃外卖了,真的难吃且贵。

有时候也会去绿城主场看看绿城踢球吧,虽然心里支持的四川队,但是远在他乡没有办法了。

最后附上自己毕业答辩后拍的照片,算是对自己充实的大学生活的怀念吧。

总之,我觉得自己每天都过得很充实吧,很热爱自己的生活。

愿每一个你们的生活都过得幸福。

基于Volley框架的返回数据的范型处理

在平时最普通的Volley的网络请求中,我们StringRequest是这样请求网络数据的:

StringRequest stringRequest = new StringRequest("http://www.baidu.com",
        new Response.Listener<String>() {
            @Override
            public void onResponse(String response) {
                Log.d("TAG", response);
            }
        }, new Response.ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError error) {
        Log.e("TAG", error.getMessage(), error);
    }

注意在onResponse的时候是拿到的string类型,拿到string后对其再进行相关的解析,我们是否是可以对其直接封装然后拿到具体想要类型的model数据结构呢?所以对其网络请求架构进行一次封装,到达如下效果:

GetGoodDetailByGidRequest getGoodDetailByGidRequest = new GetGoodDetailByGidRequest(mCouponId,
           new RequestListener<List<CouponModel>>() {
               @Override
               public void onSuccess(List<CouponModel> result) {
               }
               @Override
               public void onError(Exception e) {
                   e.printStackTrace();
                   stopLoadingDialog();
               }
           });

这里我们在构造Request的时候指定了返回数据的类型,这样的话就方便了我们在写业务的时候直接使用解析好的数据结构,具体如何做到的呢?

一、让每个Request基于一个带有范型请求类

public abstract class BaseApiRequest<T>  

这里的T就是目标请求期望的model类
在具体实现的时候继承基类,并指定返回类型,下面是一个例子:

public class GetGoodDetailByGidRequest extends BaseApiRequest<List<CouponModel>> {
    public static final String url = CURL.GoodDetailURL;
    public GetGoodDetailByGidRequest(String goodId, RequestListener<List<CouponModel>> requestListener) {
            super(requestListener);
            this.mUrlParams.put("id", goodId);
    }
    @Override
    public String getBaseUrl() {
            return url;
        }
}

二、在基类中构造网络请求

  protected StringRequest getStringRequest() {
    return new StringRequest(requestMethod, getRequestUrl(),
            response -> parseJson(response),
            error -> requestListener.onError(error)) {
        @Override
        protected Map<String, String> getParams() {
            return mEntityParams;
        }
    };
}

在此处实现可以看到Request在基类中进行,然后分别处理返回结果

三、对返回结果进行解析

    private void parseJson(String response) {
    int responseCode = 0;
    int errorCode = 400;
    try {
        JSONObject jsonObject = new JSONObject(response);

        String resultString = jsonObject.getString("data");

        if (jsonObject.has("code")) {
            responseCode = jsonObject.getInt("code");
        }
        if (jsonObject.has("error")) {
            errorCode = jsonObject.getInt("error");
        }

        if (responseCode == 200 || errorCode == 0) {
            if (!TextUtils.isEmpty(response)) {
                Type type = getTType(requestListener.getClass());
                //泛型是实体或者List等类型
                T t = JsonUtils.fromJson(resultString, type);
                requestListener.onSuccess(t);
                return;
            }
            ToastUtils.showToast("Data is empty!");
        }
        ToastUtils.showToast("Response code is error.");
        requestListener.onError(new ParseError());
    } catch (JSONException e) {
        ToastUtils.showToast(e.toString());
        e.printStackTrace();
    }
}

这里是最关键的一步,由于和后端约定好相关返回字段,那么只需要解析字段中目标model的数据,其中比较重要的是这段代码

Type type = getTType(requestListener.getClass());
//泛型是实体或者List等类型
 T t = JsonUtils.fromJson(resultString, type);
 requestListener.onSuccess(t);

通过封装好的 JsonUtils将String转化为对应的model类型,我们知道json转实体对象的时候,需要指明其类type,那这里的type是如何获取到的呢?

其中getTType ()的具体实现为:

 public static Type getTType(Class<?> clazz) {
    //以Type的形式返回本类直接实现的接口.
    Type[] types = clazz.getGenericInterfaces();
    clazz.getInterfaces();
    if (types.length > 0) {
        //返回表示此类型实际类型参数的 Type 对象的数组
        Type[] interfacesTypes = ((ParameterizedType) types[0]).getActualTypeArguments();
        return interfacesTypes[0];
    }
    return null;
}

通过次方法能够获取到请求实现中所指明的请求类型,其中getGenericInterfaces等相关原理可以阅读:https://my.oschina.net/617669559/blog/3012228

所以对于

public class GetGoodDetailByGidRequest extends BaseApiRequest<List<CouponModel>>

那么获取到的就是List类型

四、通过Listener回调相关解析结果

拿到解析好的result并回调给构造Request方法中的listener使用

T t = JsonUtils.fromJson(resultString, type);
requestListener.onSuccess(t);

这样对整个网络请求后的返回数据直接进行解析方便多了。

总结:

1、本文最主要是对基本Request类进行改造,以达到不需要每次重复写解析返回的String数据

2、在获取目标的类的类型的时候,主要是去获取基类中的“T”类型

3、设计不仅适用用Volley同样适用于其他类似的网络请求框架

小弟不才,如有问题,欢迎指出。

Java反射中getGenericInterfaces和getInterfaces的解读

今天在做解析网络请求后得到的数据的转化的时候用到了:getGenericInterfaces这个方法。

 /**
  * 获取回调接口中 T 的具体类型
  *
  * @param clazz
  * @return
  */
   public static Type getTType(Class clazz) {
    //以Type的形式返回本类直接实现的接口.
    Type[] types = clazz.getGenericInterfaces();
    if (types.length > 0) {
        //返回表示此类型实际类型参数的 Type 对象的数组
        Type[] interfacesTypes = ((ParameterizedType) types[0]).getActualTypeArguments();
        return interfacesTypes[0];
    }
    return null;
}

其中回调接口为:

new RequestListener>() {
   @Override
    public void onSuccess(List result) {}

在解析数据的时候这样操作,目的是为了对所有返回的数据进行数据转化为所指定的类型:

Type type = getTType(requestListener.getClass());
     //泛型是实体或者List等类型
     T t = JsonUtils.fromJson(resultString, type);
     requestListener.onSuccess(t);`

类RequestListener为:

public interface RequestListener {
    void onSuccess(T result);
    void onError(Exception e);
}

使用Gson进行json的解析,T fromJson(String json, Type typeOfT);那么怎么才能获取到RequestListener中的的类型呢?
于是我们从接口获取参数化类型处理。

官方文档解释

getGenericInterfaces:

Returns the {@code Type}s representing the interfaces directly implemented by the class or interface represented by this object.释意:返回表示由此对象表示的类或接口直接实现的接口的{@code Type}。

getInterfaces:

Determines the interfaces implemented by the class or interface represented by this object.
释意:返回由此对象表示的类或接口实现的接口。

从解释上面来看出来了,差异在于“接口实现的接口的Type”,接下来用具体示例来解释区别

private class Food{
    String foodName;
}
private interface Eat{
    void eat(String things);
}
private interface Run{
    void run();
}
private class Dog implements Eat,Run{
    @Override
    public void run() { }
    @Override
    public void eat(String things) { }
}
private void main() {
    Class clazz = Dog.class;
    Type[] genericInterfaces = clazz.getGenericInterfaces();
    Class[] interfaces = clazz.getInterfaces();
}
运行结果
![](https://oscimg.oschina.net/oscnet/245442107557694aef0f07c25be0740187c.jpg)

我们可以看到,clazz.getGenericInterfaces()与clazz.getInterfaces()并没有任何差异。因为 并没有:“实现的接口的Type”

接下来看另一段代码,我们对Eat接口改造一下,增加一个参数化类型

private class Food{
    String foodName;
}
private interface Eat{
    void eat(T things);
}
private interface Run{
    void run();
}

private class Dog implements Eat,Run{
    @Override
    public void run() { }
    @Override
    public void eat(Food things) { }
}
private void main() {
    Class clazz = Dog.class;
    Type[] genericInterfaces = clazz.getGenericInterfaces();
    Class[] interfaces = clazz.getInterfaces();
}
运行结果:

关于位运算和HashMap中一个求最小2次幂的算法

今天在HashMap的内部源码的时候,看到这样一个算法:

/**
 * Returns a power of two size for the given target capacity.
 * 返回大于或等于 cap 的最小2次幂
 */
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

第一眼看起来确实是一脸懵逼,通过只知道这是一个获取该数的大于或等于 cap 的最小2次幂,这么厉害,咋实现的呀?

1、数据在内存中如何存储?

我们知道”<<” 和 “>>” 分别代表 左移和右移位运算符号,表示 乘以2 和除以2(大多数时候适用),”>>>”还是第一次见,这是代表什么意思呢?说到这里我们不得不去了解一下数据是如何存储在内存中的:

在32位的计算机系统中,int型数据占几个字节? 4字节。其中每个字节有8个比特位,表示二进制位,位是计算机内部数据储存的最小单位。这是所有编程语言学习者都知道的。也就是说 int类型在内存中有4*8 == 32个比特为 所以如果以整形数10为例,那么它在内存中完整存储的形式为:

00000000 00000000 00000000 00001010 ->对应 1x2^3+0x2^2+1x2^1+0x2^0 =10

那么int类型表示最大的数是不是就是:

11111111 11111111 11111111 11111111 ->对应 1x2^31+1x2^30…1x2^1+1x2^0

但为我们知道int类型的最大值为:2^31-1,显然上面的答案不是正确的。

这是因为在所有被int类型占用的比特位中,左起第一个位(即最高位)就是符号位。int类型的符号位上,0表示正数,1表示负数。在32位操作系统下,其余后面31位是数值位。也就是说:

11111111 11111111 11111111 11111111 所代表的数字为:1x2^30+1x2^29…1x2^1+1x2^0 的相反数为:-(2^31-1)

这里需要注意的是,按原先的逻辑去理解的话

00000000 00000000 00000000 00000000 为+0

10000000 00000000 00000000 00000000 为-0

那他们表示的意义是一样的么?
实际上,在32位系统下int类型中,我们计算机已经强行规定了这种情况,数字0采用“+0”的表示方法,即 00000000 00000000 00000000 00000000;而“-0”这个特殊的数字被定义为了-2^31。

因此我们看到32位系统下int类型的取值范围中,负数部分比正数部分多了一个数字,正数的最大取值是2^31-1,而负数的最小取值是-2^31。正数部分之所以要减去1,是因为被数字0占用了“+0”,而负数部分不需要用来表示0,因此原本的“-0”就用来表示-2^31这个数字。

2、位运算如何进行?

至此我们明白了数据在计算机中的存储形式,那位运算具体怎么运行的呢?
以10和-10为例,其二进制完整表示为:00000000 00000000 00000000 00001010 和 10000000 00000000 00000000 00001010 为了便于观察,我们取后面8位:00001010

  • 对于符号位移

例如将10的二进制向左移1位:那么变成 0001010 0 == 20 原先二进制数的第一位被移除,而最后一位被舍弃。将10的二进制向右移1位 原先二进制数最后一位被移除,第一位补0,则变成 000101 ==5

如将-10的二进制向左移1位, 10000000 00000000 00000000 00001010则变成:

10000000 00000000 00000000 0010100 为-20

如将-10的二进制向右移1位, 10000000 00000000 00000000 00001010则变成 :

注意这里多了一个0-> 1 00000000 00000000 00000000 0000101 <-注意这里少了位

也就是说符号移动,会保留原来的符号位,不会因为右移左移而带走符号位。

  • 对于无符号位移

相反无符号位移会不关注符号位。
例如将-10向右无符号右移就会变成:

010000000 00000000 00000000 0000101 变成了一个很大的正数了!!

如果将-10无符号左移,则变成:

00000000 00000000 00000000 00001010 = 20

但是!!并没有无符号左移动这样一件事情!
跟右移运算不同的是,无符号左移和左移是一样的。因此java没有无符号左移运算。(<<<和<<<=将报错)

因为无符号右移运算需要考虑符号位的右移,而符号位只存在于二进制表示的最左边,最右边没有。所以不用区分无符号左移和左移运算。

3、关于返回大于或等于 cap 的最小2次幂的算法

我们以传入10为例子

由这张图看起来,算法很容易懂了,其实最主要的是为了去让各个位从高到低 从0变成1或者维持1不变,这样就能找到该数最小的2次幂

另外,需要注意一下的是,第一步 int n = cap - 1; 这个操作,执行这个操作的主要原因是为了防止在cap已经是2的n次幂的情况下,经过运算后得到的结果是cap的二倍的结果,例如如果n为l6,经过一系列运算之后,得到的结果是0001 1111,此时最后一步n+1 执行之后,就会返回32,有兴趣的可以自己进行尝试;

小弟不才,如有问题,欢迎指出。

参考来源:

https://blog.csdn.net/c10WTiybQ1Ye3/article/details/89411471
https://www.jianshu.com/p/927009730809

Java虚拟机类的加载机制

什么是虚拟机类的加载机制?

虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换,解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

类从加载到虚拟机的内存中开始,直到卸载出内存为止,整个生命周期为:

  • 加载(loading)

  • 验证(verification)

  • 准备(preparation)

  • 解析(resolution)

  • 初始化(initialization)

  • 使用(using)

  • 卸载(unloading)

其中 验证、准备、解析部分统称为连接

接下来依次讲解,各个步骤所做的事

第一部分 加载


“加载”是“类加载”的一个阶段,注意区分概念。类的加载由类加载器(后面介绍)加载主要完成三件事情:

1、通过一个类的全限定名来获取其定义的二进制字节流。

2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

加载阶段完成后,虚拟机外部的二进制字节流将按照虚拟机所需的格式存储在方法区中,同时在内存中实例化一个java.lang.Class的实例对象。相对于HotSpot,这个实例对象比较特殊,虽然是一个对象,但并没有放置在堆中,而是放置在方法区中。这个对象将作为程序访问方法区中这些类数据的外部接口。

第二部分 验证


这一步主要是确保Class文件的字节流符合虚拟机的规范

主要验证以下几个部分:

1、文件格式验证
验证是否以魔数开头、主次版本号是否在当前虚拟机处理范围内…

这一验证阶段主要是保证输入的字节流能正确地解析并存储与方法区内,格式上符合Java类型信息的要求。只有通过这个阶段,字节流才会进入内存的方法区中存储,后面的三个验证方式也都是给予方法区中的数据验证,不再会操作字节流。

2、元数据验证 验证这个类是否有父类、这个类是否继承了不允许继承的类…

该阶段主要对类的元数据进行语义校验,保证符合java语言规范的元数据信息。

3、字节码验证

最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是否合法、符合逻辑。

4、符号引用验证

这个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作发生在连接的解析阶段。目的是确保解析动作正常执行,如果无法通过验证,将抛出 IllegalAccessError、NoSuchFieldError、NoSuchMethodError等异常。

第三部分 准备


准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所实用的内存将在方法区中进行分配。

这时候的分配仅仅是类变量(staic修饰的变量),而实例变量将会在对象实例化时随对象一起分配在Java对中。

假设一个类变量为:public static int count = 10;这时候会分配0,而不是10,分配10是在程序编译后。

第四部分 解析


解析阶段是虚拟机将常量池的符号引用替换为直接引用的阶段

1、类或者接口的的解析

2、字段解析

3、类方法解析

4、接口方法解析

符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

第五部分 初始化


在准备阶段,变量已经被分配赋值过初始值,在初始化阶段根据代码的逻辑初始化真实的变量和其他资源。

关于类加载器


什么是类加载器?

在“加载”阶段中,通过一个类的全限定名来获取其定义的二进制字节流。这一动作是放到了Java虚拟机外部去实现的,是为了方便让应用自己去决定如何获取所需要的类,实现这个动作的功能是常说的“类加载器(ClassLoader)”

类加载器主要有三种:

1.启动类加载器(Bootstrap ClassLoader)

负责加载<JAVA_HOME>\lib

2.扩展类加载器(Exension ClassLoader)

负责加载<JAVA_HOME>\lib\ext

3.应用程序类加载器(Applicaion ClassLoader)

负责加载ClassPath上指定的类库

类加载器工作原理

介绍类加载器原理之前,必须得了解双亲委派模型(Parents Delegation Model)

双亲委派模式的工作原理的是:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。

如图所示,这种层次结构关系被称为双亲委派模型
以下为其实现代码,集中在java.lang.ClassLoader中的loadClass()方法中

    protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
    {
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
    //首先检查类是否被加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
        //如果有父加载器,则先委托父加载,否则由启动类加载器加载,如果启动类加载器没有找到,则返回null
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
        //这里的ClassNotFoundException来自父加载器
            }
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
        //在父类Classloader还没办法加载的时候
        //再调用本身的findclass方法来加载类
                long t1 = System.nanoTime();
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

代码的逻辑很清楚:

先加载类是否已经被加载过,若没有则调用父的loadClass()方法,如果父 类加载器为空,则使用启动类加载器作为父加载器,如果父 类加载器加载失败,再调用自己的findClass()方法进行加载

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×