博客

  • 「杂谈」论“官话”——无方法的方法论

    叠甲与前言

    我只是个高中学生,没有任何社会经验。——所以,请你不要把这篇文章看得太过认真。

    那么就开始吧。

    我是一个政治高考选考学生。在我刚开始接触政治课本中的语言时,我只觉得这简直是一堆由一堆长难句复合而成的更长更难的长难句,然后拿标点符号胡乱组起来的天书,这样读书,无疑是十分具备挫败感的。

    后来,随着做题量的增加,我逐渐入门了这种奇妙的语言,我开始感觉自己有了对这些套话的掌握。

    但现在,我发现这套语言的信息密度几乎为 0。

    所以我逐渐开始对其产生一点厌恶的感受了。

    最近,我又刷到一个相关内容的视频,于是就打算写篇文章。

    官话套话当然既是智慧的也是愚蠢的。但这里,我只批评它。

    你们应该怎么做和我有何干系

    我先解释一下我的标题吧。

    “无方法的方法论”。

    为什么这样说呢?其实上面提到的视频里有一个有趣的观点:官话套话作为一套语言模板,却把沟通与交流的作用逐渐隐去,便只剩下一层繁复的语言仪式。

    我问你,——

    你这套方案的“底层逻辑”是什么?

    你解决这个问题的“抓手”在哪里?

    啊,我的底层逻辑是……抓手就在……。

    恭喜你,你直接成为了毛泽东批评过的党八股。——有些同志,写文章啊,特别喜欢写大壹贰叁肆,然后就是小一二三四,完事还有甲乙丙丁,再不够还有大 ABCD 小 abcd 呢。这样的文字是最荼毒的,因为它只是按照事物的外部概念给事物分分类,俗称“开中药铺”。看似抓住了全局要害,实则没有任何用处。

    人人都知道解决问题的底层逻辑与抓手固然是好的。

    但我要问你,你到底要怎么解决它?

    其实没有人清楚。——我找到抓手就行了,怎么抓住它与我有什么关系呢?我只管发号施令就是了,到底具体怎么做可以讨我欢喜,你们下面就自己做着看嘛。

    于是两袖一挥,发现问题,却不提出怎么解决问题,最后还写完了一篇辞藻丰富内容充实的报告,真是妙哉。

    人人都在谈“抓手”、“闭环”、“压实责任”,但谁知道明天第一步到底该怎么做呢?

    最近政治又恰好学进了哲学部分,我想,这样一种拥有“方法论”的全部权威和形式的语言,目的却是为了消灭方法本身,本身就是一种“没有方法的方法论”。

    官话发展出一套成熟的“方法论”话语体系,其功能却并非指导实践、解决问题,而是通过一套繁复的语言仪式,回避任何具体方法,在绝对安全的境地里,维持一种“一切尽在掌握”的幻象。

    快来帮领导的报告提供材料

    还有视频里没提到的一点:官话套话的模板有时会抬高一件小事本身的价值与意义。

    我最近写到一道政治题,题干材料是这样的:……因荔枝“一日色变,二日香变,三日味变”的特性,古人称其为“离枝”。如今,我国采取冻眠技术,借助超低温冷冻锁鲜技术,解决了荔枝长时间贮藏保鲜问题,打破荡枝销售“只争朝夕”的限制,填补秋冬季荔枝市场空白,实现荔枝“全年卖、卖全球”。……

    我乍一看,哦,古代科技不发达,我们都要金克拉……哈哈,串台了,是古代科技不发达,现代人们解决它。

    “我国采取……”更是直接点明我国科技从追赶到超越的倔强赶超,从 0 到 1 再到领跑的攻坚克难,我们中国真是太厉害了!我看着卷子,哭得像个小泪人,一股强烈的民族自豪感在心中油然而生,一时甚至忘记了看选项写下答案,脑子里只有满满的对我国科技发展如此迅猛之势的深刻赞叹。

    但只要你仔细一琢磨,就会发现,这不过是和我抄的作业因为太好了被老师表扬一样有趣。

    我首先不太理解这个“冻眠技术”,特地去查了一番,冻眠在20世纪90年代由日本泰克尼康株式会社社长山田义夫发明。2018年5月31日,山田义夫代表日本泰克尼康株式会社,在纽约联合国总部首次进行题为《冻眠技术能改变什么》的演讲。2018 年这项技术被引入中国。

    再说这个“超低温冷冻锁鲜技术”,好像具体没查到是哪项技术。可能是美国的液氮低温,也可能是日本的 CAS(同样 20 实际 90 年末研发)。

    “我国采取冻眠技术…解决了…填补了…实现了…”的叙事,刻意模糊了主语,营造出“国家主导的科技攻关取得重大原创突破”的观感,看得我差点颅内高潮。

    只是从别的国家拿了几项技术,就又是解决了、打破了、填补了、实现了,真好像是什么功在当代、利在千秋的超级工程呢。

    只是复用了一下别人成熟的经验与路线,就是“只争朝夕”与“打破…限制”的戏剧冲突,真好像有一堆科研人员攻坚克难得之不易的伟大结果呢。

    不过这样难得的材料,上边呐,要写进自己的报告里,却是最美的了。材料当然不是拿来客观描述事实的,是用来朝别人邀功炫耀的啊!

    啊!写到这里,我双眼又湿润了。

    这真是这世界上最伟大、最振奋人心、最令人激动无比的,对这“伪方法论”的最究极、最终极的颂歌!

    我也要励志为国家做贡献!我再也忍不住,拿起笔就酣畅淋漓地写:

    《关于破解传统摩托“费油”沉疴 开创绿色出行新纪元的成果汇报》

    自古以来,传统燃油摩托车因其“一日耗油,二日费钱,三日污染”的固有特性,长期困扰群众出行,成为制约绿色交通发展的“拦路虎”。古人无奈称其为“油老虎”。面对这一世界性难题,我国科技工作者不畏艰险、集智攻关,成功开辟出一条颠覆性的技术路线,彻底打破了燃油动力的“路径依赖”,实现出行成本“断崖式下降”,填补了家用两轮零油耗市场的空白,真正让“绿色出行、全域畅达”成为现实。

    一是坚持问题导向,以“无中生有”的魄力定义全新物种。
    面对摩托车“无油不走”的铁律,我们跳出传统内燃机“修修补补”的老路,发扬“敢教日月换新天”的首创精神,首次提出“将燃油系统彻底移除”的大胆构想。这是一次从“0到1”的原始创新,完全摆脱了对石化能源的束缚,创造性地定义了一款无油箱、无火花塞、无尾气排放的新概念载具。

    二是坚持系统思维,用“最经济”的方案解决“最顽固”的症结。
    为解决传统摩托“油门一响,钱包见底”的成本痛点,我们摒弃了复杂的机械传动结构,代之以高密度储能单元与轮毂直驱系统。这套方案不仅实现了运行过程中的“静默”与“零碳”,更将百公里能耗成本降至传统摩托的十分之一。用群众听得懂的比喻来说,就是“啃下了出行成本的硬骨头,牵住了绿色动力的牛鼻子”。

    三是坚持人民至上,将“实验室的盆景”变为“百姓家的风景”。
    成果好不好,百姓说了算。目前,这款被群众亲切称为“电驴”的颠覆性产品,已形成数以亿计的庞大市场规模。它不讲空头术语,不搞高价壁垒,用实实在在的“每公里三分钱”,让科技的温情流淌进千家万户。它不仅是一款产品,更是一份写在大地上的“民生答卷”。

    总结而言, 这是一次打破能源垄断的翻身仗,是思维解放结出的硕果。我们用事实证明:只要扔掉思维里那台“费油的发动机”,就能发动科技创新的“永动机”。未来,我们将继续以此为范本,推动更多“看似不可能”的技术变革,为全球绿色出行贡献出独一无二的“中国方案”。

    ——你管我是不是真的做出了什么巨大的成就呢,反正这些句子,这堆又臭又长的裹脚布,你看都懒得看,就默认我是大发明家爱迪生呗。

    生动过度,就是死气沉沉

    哦我差点忘记了,还有那些你妈的生动比喻。

    比如“眉毛胡子一把抓”、“牵牛鼻子”、“啃硬骨头”,这些话固然可以降低大家的理解成本,也可以为冰冷的权力的法杖披上一层人情味的外衣。枯燥的抽象概念(如“工作无重点”、“抓住主要矛盾”)很难在基层执行者脑中形成画面。而“牵牛鼻子”,瞬间就让所有人都知道该集中力量攻关键点。从这个角度看,生动的比喻倒也并不是一无是处。

    但是,如果大家除了用这些比喻,就不会说话了呢?

    我不管具体情境,不管具体矛盾,不管具体困难,反正上来就是一个“我们要先牵住牛鼻子”“要找到抓手”,这样的语言万金油从我的嘴里吐出来,毫无意义。

    我只管让你们牵牛鼻子就是咯,至于怎么牵,具体牵哪里,多大力气牵之类的问题,

    关我屁事啊?

    欸~,这就是他的精妙之处了。现实往往“一团乱麻”,肯定需要人去系统梳理再解开,但我比较懒,怎么办呢?那我就只管大声喊“我们要快刀斩乱麻”的口号就行了。

    一句生动的比喻,就能掩盖过所有矛盾的复杂性,性价比真高。

    什么?你们有人说我是大空话,言之无物?你要不再睁大眼睛看看,我这可是生动的比喻句!

    不知不觉我就写了三千多字了。让思绪自然流淌写文字,确实比绞尽脑汁考场上写 800 字作文来的容易得多了。

    还有一些我想说的,比如说内部的“密码本”、责任人直接隐身之类的官话的“好处”,由于文章开头提到的视频里都提到了,我也认同,所以就不再多写了。

    官话套话固然有好处的,我并不是想故意放大它糟粕的一面。但是这糟粕,必须要让大家引起注意。

    给大家一种掌控感终究是好的,就算是虚假的,也可以让这个社会循规蹈矩地安静下来。

    但,应该没人想一堆亟待解决的大问题,写进报告里就只是一句“大局稳中向好,局部需要加强”的大空话吧?(笑

  • 「杂谈」浮生若梦

    碎碎念

    按照浙江省 80 岁的平均寿命,我的生命过去了快 1/5;

    而在公元纪年的 100 年里,21 世纪也刚过去了 1/4。

    这似乎不足为提。但或许是总用地质纪念的眼光去看世界的缘故吧,我越发觉得我们从来该没有创造过真正有意义的东西。

    什么文明,什么艺术,在地质纪年的尺度下都只会被拍的粉碎。

    一块普通的岩石的形成,要上万年,而人类的文明从出现到现在,不过一万年。

    什么生命奇迹,什么辉煌城市,在宇宙的眼里,连一瞬都没资格算作。

    我小时候第一次看见地球与太阳体积的直观对比,以及“地球在太阳系中相当于一个房间内一粒花生米大小”的类比,我便知晓了地球的渺小。而我现在又看我们引以为傲的人类文明时,却发现人类也不过是地球上薄薄的一层灰,任何一阵风吹雨打,都会直接将人类的踪迹抹除得一干二净。

    我们力争的永恒,在地质学眼里连瞬间都谈不上。

    我们现有的文明,在宇宙的眼里连一道刻痕都不是。

    ——那,“意义”这个词,又究竟还有什么意义?

    地壳运动

    我从地理课本上得到了很多知识,也包括地球诞生以来多次的大规模地壳运动。

    地壳运动让陆地露出海面,让哺乳动物的诞生有了前提条件。

    可是,下一次大规模地壳运动会在什么时候?

    地壳运动是地球的赠礼,也是地球人类文明的终结。我们从哪里来,就要到哪里去,——这样似乎倒也确实挺合理。

    从某种角度上讲,我认为人类是恐龙的后继者。

    人们依靠残存的化石证明恐龙的存在。即使自认为比它们文明得多,却也给它们一个“地球霸主”的称号。

    要是待到以后人类灭绝,新的,或许更加高级的物种,也会挖掘出人类的城市遗迹,了解到这个曾经在曾经存在过的曾经。

    地壳的运动就像谢幕又开场的舞剧,毫无目的、荒诞地,却也是平静的呼吸。地球上的生命就像一层薄沙,地球一吸一呼,就是一个文明的开始与结局。

    人类自以为是地感谢着地球的“馈赠”,却没有想过,地球从来都没有施舍过谁,偏袒过谁,又或者是专门去费尽心思制造一个“生命奇迹”

    ——地球不在乎人类。

    朦胧

    中考完的暑假,我开了个 MC 服务器,与同学们一起玩了两个月。

    我几乎什么也没干,只是每天上线,看到这里多了一台生电机器,那里又多了一个雕塑或是建筑。

    我当时载了一个低配的光影,但只要有水体的反射、光线的计算,加上树叶的晃动,就已经够美了。

    我们用樱花木盖了一座粉粉的别墅,依傍着一条河流。

    我们在阁楼上养了十多只猫。

    我从来是养老玩家。自己一个人打开 MC,种田,然后就关掉。

    我记得那甚至是我第一次使用鞘翅。

    闲着没事干,就坐在一起钓钓鱼——一群人坐在一排椅子上——我的鱼掉落物总是被旁边的人吸走。

    又或者是随地开始 PVP,没有记分板,大家只是单纯享受着互相按鼠标的乐趣。

    后来,大家的时间也不怎么多,互相也就疏远了。当时的人,现在再也召集不齐来玩游戏了。

    现在,服务端的存档依然保留在我的 OneDrive 里。

    或许那樱花树林还在抛洒着落叶,或许房子旁还有牛羊猫狗的呼噜声,或许河流里还有我们从来没钓上来过的宝藏。存档依然在我的云盘里

    ——但里面真的已经没有人了。我们也再也回不去了。

    花火,或是灰烬

    我现在最难过的事情,就是猛然发现自己其实什么也留不住。

    就算我在空间上停止移动,我依然以光速在时间上移动。

    我或许有两条道路可选:

    一是就这样消沉下去,永远也找不到、抓不住意义的存在。

    二是……我做不到二。

    二当然就是罗曼罗兰那句人尽皆知的“世界上只有一种英雄主义,就是看清生活的真相之后,依然热爱生活。”

    但我实在做不到。

    这种时常伴我的幻灭感,或许以后也会成为一份礼物吧。

    就是因为害怕失去,所以才会更加用力地去珍惜。

    就是因为害怕错过,所以才会更加无畏的去做。

    就是因为害怕凋零,所以才会更加热烈的去爱,贪婪地享受着自己现有的、仅有的每一瞬间。

    也就是因为恐惧虚无,所以才会抓住着梦里的每一寸光阴,在这幻灭的薄雾里竭力创造着无意义的欢愉。

    中国神话《精卫》里,大海同宇宙当然是不可能被坚毅战胜的,中国人的祖先们认为,只要自己的意志是永恒的,沧海也能成为桑田。

    但我常带着另外一种眼光看这故事:我若是那只精卫,我不会去执着于衔着木枝与石子往返于陆与海,我大概率不会这样做。——向海里丢石头,究竟是我想改变世界,还是我只是认清了自己只能做这些事情的现实?

    我若是那只精卫鸟,或许我不会执着于把海填平,我会在空中盘旋,盘旋,盘旋,一直到最后一丝力气耗尽,再坠落在这大洋里。

    这里可能便有人要发问:你这样做是荒唐的啊,既填不了海,也浪费了自己的一生。

    但是不要忘了,我们上面铺垫过了,所谓“意义”,其实到最后都是无意义的。

    ——飞蛾扑火,好歹还向死而生呢。

    沉入深海,

    下沉,

    下潜。

    既然无法跳脱出这个梦,就更狂热地彻底融入它!

    这是我的终结,却是意义的起点。

    潮水终将抹平沙滩,但每一次退潮后,总有新的东西在生长。

    就像你我明知道浮生若梦,却仍在清晨醒来,去爱,去创造,去度过这珍贵又荒诞的一天。

    ——虚无主义固然是可怕的。

    因为一切物质都处于运动之中,一切事物都在诞生与消逝的轮回中。

    但,走向泯灭,说不定就是生命本该有的目的呢?

    不知道是不是因为视雪症的缘故。

    总之就是这些了。

    写的很碎,感谢你读完。

  • Java 入门 学习笔记:多线程 & JUC

    什么是多线程

    线程

    线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

    进程

    进程是程序的基本执行实体。

    并发与并行

    并发:在同一时刻,有多个指令在单个CPU上交替执行。

    并行:在同一时刻,有多个指令在多个CPU上同时执行。

    多线程的三种实现方式

    ① 继承 Thread 类的方式进行实现

    可以声明一个 Thread 的子类,重写 run 方法:

    public class demo {
        public static void main(String[] args) {
            MyThread t1 = new MyThread();
            MyThread t2 = new MyThread();
            t1.start();
            t2.start();
        }
    }
    public class MyThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("Print");
            }
        }
    }

    这里我自定了一个 MyThread 类,为了更好地验证多线程的执行,我们可以使用 setName 给线程起一个名字并一同打印:

    public class demo {
        public static void main(String[] args) {
            MyThread t1 = new MyThread();
            MyThread t2 = new MyThread();
    
            t1.setName("Thread1");
            t2.setName("Thread2");
    
            t1.start();
            t2.start();
        }
    }
    public class MyThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(getName() + "Print");
            }
        }
    }
    

    ② 实现 Runnable 接口的方式进行实现

    自己定义一个对象实现 Runable 接口,重写 run 方法,创建自定对象与 Thread 对象并将自定对象传入后者:

    public class demo {
        public static void main(String[] args) {
            MyRun mr = new MyRun();
            Thread t1 = new Thread(mr);
            Thread t2 = new Thread(mr);
            t1.setName("Thread1");
            t2.setName("Thread2");
            t1.start();
            t2.start();
        }
    }
    public class MyRun implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + ": Print");
            }
        }
    }

    由于是接口,没有继承关系,所以要先获取到当前对象,然后 getName 获取到的对象。

    ③ 利用 Callable 接口和 Future 接口方式实现

    这种实现方式特点就是有返回值,可以获取多线程运行结果。同时也可以抛出异常。

    例如,我们需要求得1加到100的和,这里我创建一个 MyCallable 类实现 Callable 接口并重写 call 方法,然后创建 MyCallable 对象(表示需要执行的任务)与 FutureTaskk 对象(用于管理多线程),以及 Thread 对象并启动:

    import java.util.concurrent.FutureTask;
    
    public class demo {
        public static void main(String[] args) throws Exception {
            // 创建 MyCallable 对象
            MyCallable mc = new MyCallable();
            // 创建 FutureTask 对象
            FutureTask<Integer> ft = new FutureTask<>(mc);
            // 创建线程对象
            Thread t1 = new Thread(ft);
            t1.start();
    
            Integer result = ft.get();
            System.out.println(result);
        }
    }
    import java.util.concurrent.Callable;
    
    public class MyCallable implements Callable<Integer> {
        @Override
        public Integer call() {
            int sum = 0;
            for (int i = 0; i <= 100; i++) {
                sum += i;
            }
            return sum;
        }
    }
    

    相较于前两种方法,由于不能直接继承 Thread 类,操作相对复杂。但是拓展性强,因为在实现了 Callable 接口后仍然可以继承其它类。

    多线程中常用成员方法

    setName、getName

    字面意思。

    Thread currentThread()

    获取当前线程的对象。

    sleep(long millis)

    让线程休眠指定的时间,单位为毫秒。

    setPriority(int newPriority)、final int getPriority()

    设置、获取线程的优先级。

    final void setDaemon(boolean on)

    设置为守护线程。

    yield()

    暂停当前正在执行的线程对象,并执行其他线程。

    public static void join()

    插入线程/插队线程。

    线程的优先级

    线程的调度

    抢占式调度:随机执行线程。具有随机性。

    非抢占式调度:轮流执行线程。

    优先级越大,那么随机概率就越大。优先级由 1 到 10,默认 5。

    守护线程

    非守护线程全部结束时,JVM 就会退出,同时所有守护线程自动终止。

    礼让线程

    Thread.yield()

    当前正在执行的线程暂停一下,主动让出 CPU 资源,从“运行状态”回到“就绪状态”,让 CPU 重新选择线程执行(可能还是会选回这个线程自己)。

    插入线程

    Thread.join()

    让当前线程等待另一个线程完全执行完毕(死亡)之后,再继续执行自己后面的代码。俗称“插队”:要等到某个线程的任务全部完成后,自己才继续往下走。

    例如,我若想把 t 线程对象插队,书写 t.join(); 语句即可。

    线程的生命周期 / 6 种状态

    Java 里用 Thread.State 枚举定义了线程的 6 种状态:

            新建
    
            就绪 ←——←——←——←——←
             ↓                   ↑
            运行                  ↑
             ↓   (等待锁)        ↑
           阻塞 ——→ 获取锁成功 ——↑
             ↓   (需主动唤醒)
           等待 ——→ 被通知 ——→ 就绪
             ↓   (计时等待)
          计时等待 ——→ 通知/超时 → 就绪
    
           终止

    线程的安全问题

    什么是线程安全

    多个线程同时访问同一个共享资源,且最终结果与单线程执行一致时,就说这个程序是线程安全的。否则就是不安全的。

    为什么会出现问题

    核心原因有三个:原子性、可见性、有序性:

    • 原子性:一个操作是不可分割的,不能被打断。例如 i++ 实际是“读-改-写”三步,不是原子操作,多线程同时执行会互相覆盖。
    • 可见性:一个线程对共享变量的修改,另一个线程不能立即看到。因为每个线程有自己的工作内存,数据缓存在寄存器或 CPU 缓存中。
    • 有序性:编译器和处理器可能会对指令重排序,在多线程下导致意外结果。

    以买票为例:

    public class UnsafeTicket implements Runnable {
        private int tickets = 100;
    
        @Override
        public void run() {
            while (true) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(10); // 模拟出票耗时,放大问题
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() 
                                       + " 卖出一张,剩余:" + --tickets);
                } else {
                    break;
                }
            }
        }
    
        public static void main(String[] args) {
            Runnable task = new UnsafeTicket();
            Thread t1 = new Thread(task, "窗口1");
            Thread t2 = new Thread(task, "窗口2");
            Thread t3 = new Thread(task, "窗口3");
            t1.start();
            t2.start();
            t3.start();
        }
    }

    很可能出现:

    • 卖出负数票(比如剩余1,两个线程同时通过 >0 判断,然后都执行了 --tickets)。
    • 同一张票被卖两次(线程1打印剩余10,线程2也打印剩余10,因为读取到的值相同)。

    这就是线程不安全。

    同步代码块

    synchronized 关键字。

    锁默认是打开的,当有线程进入,自动锁。锁内代码执行完毕后,锁自动打开。

    我们创建一个锁对象,然后用 synchronized 住操作,修改上面代码:

    public class UnsafeTicket implements Runnable {
        private int tickets = 100;
        private Object lock = new Object();
    
        @Override
        public void run() {
            while (true) {
                synchronized(lock) {
                    if (tickets > 0) {
                        try {
                            Thread.sleep(10); // 模拟出票耗时,放大问题
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() 
                                        + " 卖出一张,剩余:" + --tickets);
                    } else {
                        break;
                    }
                }
            }
        }
    
        public static void main(String[] args) {
            Runnable task = new UnsafeTicket();
            Thread t1 = new Thread(task, "窗口1");
            Thread t2 = new Thread(task, "窗口2");
            Thread t3 = new Thread(task, "窗口3");
            t1.start();
            t2.start();
            t3.start();
        }
    }

    同步方法

    直接把 synchronized 写在方法的返回值前面。

    public synchronized void sellTicket() {
        // 需要同步的代码
    }

    一次只能有一个线程访问这个对象的任意一个同步实例方法,其他想访问的线程会被阻塞,直到该方法执行完毕释放锁。

    据此,我们可以把整个卖票的动作封装为一个同步方法:

    public class UnsafeTicket implements Runnable {
        private int tickets = 100;
    
        // 同步方法,锁是 this(当前这个 Runnable 对象)
        private synchronized boolean sellOneTicket() {
            if (tickets > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()
                        + " 卖出一张,剩余:" + --tickets);
                return true;   // 表示卖掉一张
            }
            return false;      // 无票可卖
        }
    
        @Override
        public void run() {
            while (sellOneTicket()) {
                // 只要返回 true 就继续卖,返回 false 结束循环
            }
        }
    
        public static void main(String[] args) {
            UnsafeTicket task = new UnsafeTicket();
            new Thread(task, "窗口1").start();
            new Thread(task, "窗口2").start();
            new Thread(task, "窗口3").start();
        }
    }

    Lock 锁

    Lock 是一个接口,最常用的实现类是 ReentrantLock(可重入锁)。它和 synchronized 一样能保证线程安全,但提供了更多功能。

    核心方法:

    • lock():获取锁,如果锁被占用就阻塞等待。
    • unlock():释放锁。必须执行,所以通常放在 finally 块里。
    • tryLock():尝试获取锁,立即返回 true/false,不会阻塞。
    • tryLock(long time, TimeUnit unit):在给定时间内尝试获取锁,超时返回 false
    • lockInterruptibly():允许在等待锁时被 interrupt() 中断。

    死锁

    死锁是指两个或更多线程互相持有对方需要的锁,大家都出不来,程序永远卡住。

    多线程中的生产者和消费者(等待唤醒机制)

    生产者生产数据;消费者消费数据。

    常用方法

    • wait():让当前线程释放锁并进入等待状态(WAITING),直到被 notify/notifyAll 唤醒。
    • notify():随机唤醒一个在此对象上等待的线程。
    • notifyAll():唤醒所有在此对象上等待的线程。

    例如:

    public class ProducerConsumer {
        private static final Object lock = new Object();
        private static int data = 0;
        private static boolean hasData = false;
    
        public static void main(String[] args) {
            // 生产者
            Thread producer = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 1; i <= 5; i++) {
                        synchronized (lock) {
                            // 如果还有数据没被消费,生产者等待
                            while (hasData) {
                                try {
                                    lock.wait();
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                            }
                            data = i;
                            System.out.println("生产:" + data);
                            hasData = true;
                            lock.notifyAll();   // 唤醒消费者
                        }
                    }
                }
            }, "生产者");
    
            // 消费者
            Thread consumer = new Thread(new Runnable() {
                @Override
                public void run() {
                    // 简单起见,消费 5 次数据后自动退出
                    for (int i = 1; i <= 5; i++) {
                        synchronized (lock) {
                            // 如果没有数据,消费者等待
                            while (!hasData) {
                                try {
                                    lock.wait();
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                            }
                            System.out.println("消费:" + data);
                            hasData = false;
                            lock.notifyAll();   // 唤醒生产者
                        }
                    }
                }
            }, "消费者");
    
            producer.start();
            consumer.start();
        }
    }

    线程池

    上面多线程的弊端就在于:线程每次 start 需要创建,而执行完毕后又要销毁。

    一个线程被占用而另一个任务请求线程时,线程池会自动创建一个新线程。可以自行设置线程池内线程数量上限。

    可缓存线程池 newCachedThreadPool()

    • 核心线程数 = 0,最大线程数 = Integer.MAX_VALUE(几乎无限)。
    • 空闲线程 60 秒后会被回收,来一个任务就创建或复用一个线程。
    • 适合任务多、执行时间短的场景,但线程数量可能暴增。
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class ThreadPoolDemo {
        public static void main(String[] args) {
            // 创建线程池
            ExecutorService pool1 = Executors.newCachedThreadPool();
            // 提交任务
            pool1.submit(new MyRunnable());
            pool1.submit(new MyRunnable());
            pool1.submit(new MyRunnable());
            pool1.submit(new MyRunnable());
            pool1.submit(new MyRunnable());
            // 销毁线程池
            pool1.shutdown();
        }
    }

    这里我定义了一个 MyRunnable 类实现了 Runnable 接口。

    固定大小线程池 newFixedThreadPool(int n)

    • 核心线程数 = 最大线程数 = n。
    • 多余的任务放在无界队列LinkedBlockingQueue)中等待。
    • 适合任务量已知且稳定的场景(但队列无界,极端情况可能 OOM)。

    自定义线程池 ThreadPoolExecutor

    7 个参数:

    public ThreadPoolExecutor(
        int corePoolSize,              // 核心线程数
        int maximumPoolSize,           // 最大线程数
        long keepAliveTime,            // 空闲线程存活时间
        TimeUnit unit,                 // 时间单位
        BlockingQueue<Runnable> workQueue, // 任务队列
        ThreadFactory threadFactory,   // 线程工厂
        RejectedExecutionHandler handler // 拒绝策略
    )

    对于 workQueue

    保存等待执行任务的阻塞队列。

    • ArrayBlockingQueue:有界队列,防止资源耗尽。
    • LinkedBlockingQueue:无界队列(不设容量时),可能导致 OOM。
    • SynchronousQueue:不存储任务,直接交给线程,没有空闲线程则创建新线程或拒绝。

    对于 threadFactory(线程工厂):

    • 用来创建新线程的工厂,通常用于给线程起有意义的名字、设置守护线程等。
    • 默认使用 Executors.defaultThreadFactory()

    这里需要注意:

    当核心线程与队列均排满后,会创建临时线程处理后面堆积的任务。

    若上述三者都排满了,则会触发任务的拒绝策略。

    拒绝策略

    • AbortPolicy (默认) 直接抛出 RejectedExecutionException,不执行任务
    • CallerRunsPolicy 由提交该任务的线程(调用者)自己来执行
    • DiscardPolicy 默默丢弃新任务,没有任何异常
    • DiscardOldestPolicy 丢弃队列中最老的一个任务,然后重新提交当前任务

    线程池多大合适?

    最大并行数

    即 CPU 线程数。建议在 Java 中获取:

    Runtime.getRuntime().availiableProcessors();

    线程池大小计算公式

    这里有可以直接套用的计算公式。

    对于 CPU 密集型运算:

    最大并行数+1\text{最大并行数} + 1

    对于 I/O 密集型:

    最大并行数×期望CPU占用率×CPU计算时间+等待时间CPU计算时间\text{最大并行数} \times \text{期望CPU占用率} \times \frac{\text{CPU计算时间} + \text{等待时间}}{\text{CPU计算时间}}
  • Java入门 学习笔记:File

    File 概述

    • File对象就表示一个路径,可以是文件的路径、也可以是文件夹的路径
    • 这个路径可以是存在的,也允许是不存在的

    创建一个 File 对象:

    String str = "D:\\a.txt"
    File f1 = new File(str);

    我们也可以拼接父子路径:

    String parent = "D:"
    String child = "a.txt"
    File f2 = new File(parent,child);

    File 常见成员方法

    判断、获取

    • isDrectoryisFile:判断指定的 File 是否是文件夹或者文件。
    • exists:判断是否存在
    • getAbsolutePathgetPath:获取绝对或定义文件路径
    • getName:获取文件名
    • lastModified:文件的最后修改时间,返回一个毫米级 UNIX 时间戳

    创建、删除

    • createNewFile:创建新的空文件
    • mkdir:创建文件夹
    • mkdirs:创建多级文件夹
    • delete:删除文件、空文件夹

    createNewFile 方法如果遇到已存在的文件,则返回 false;如果父级路径不存在,会抛出 IOException

    获取并遍历

    public File[] listFiles() 方法,可以获取当前目录下所有文件。

    例如:

    File f = new File("D:\\aaa");
    File[] file = f.listFiles();
    for (File file : files) {
        sout(file);
    }
    • 当调用者File表示的路径不存在时,返回null
    • 当调用者File表示的路径是文件时,返回null
    • 当调用者File表示的路径是一个空文件夹时,返回一个长度为0的数组
    • 当调用者File表示的路径是一个有内容的文件夹时,将里面所有文件和文件夹的路径放在File数组中返回
    • 当调用者File表示的路径是一个有隐藏文件的文件夹时,将里面所有文件和文件夹的路径放在File数组中返回,包含隐藏文件
    • 当调用者File表示的路径是需要权限才能访问的文件夹时,返回null

    其余的获取并遍历相关方法略过。

  • Java入门 学习笔记:异常

    异常体系介绍

    规定程序出现异常后,该如何处理。

    父类:Exception。

    分为编译时异常、运行时异常。

    Exception

    叫做异常,代表程序可能出现的问题。

    我们通常会用 Exception 以及他的子类来封装程序出现的问题。

    运行时异常

    RuntimeException 及其子类,编译阶段不会出现异常提醒。

    运行时出现的异常(如:数组索引越界异常)

    编译时异常

    编译阶段就会出现异常提醒的。(如:日期解析异常)

    异常的作用

    1. 查看 BUG 关键信息
    2. 可以作为方法的特殊返回值,告诉开发者方法的底层执行信息

    我们可以用 throw new RunTimeException 语句抛出异常。

    异常的处理

    JVM 虚拟机处理异常的默认方式

    • 把异常的名称,异常原因及异常出现的位置等信息输出在了控制台
    • 程序停止执行,下面的代码不会再执行了

    try…catch 捕获异常

    在 Java 中,try-catch 用于捕获并处理运行时可能发生的异常,从而避免程序异常终止。try 块包含可能抛出异常的代码,catch 块则定义了异常发生时的处理逻辑。

    比方说捕获一个空指针异常:

    public class ExceptionDemo {
       public static void main(String[] args) {
           String s = null;
           try {
               System.out.println(s.length()); // 可能抛出 NullPointerException
           } catch (NullPointerException e) {
               System.out.println("捕获到异常: " + e);
           }
           System.out.println("程序继续执行...");
       }
    }

    可以写多个 catch 以捕获多种异常。父类异常写在最下面。例如最后一个 catch 我可以写 catch (Exception e){}

    如果 try 中的代码体执行出现问题,就会跳到下方的 catch 代码块中执行。如果 catch 中没有包括这种异常,还是交给 JVM 虚拟机处理。

    异常的常见方法

    • getMassage:获取错误信息
    • toString:获取错误名及错误信息
    • printStackTrace:底层利用 System.err 进行输出,本质上只是打印,不会停止程序运行

    抛出异常

    throws

    写在方法定义出,表示声明一个异常,告诉调用者可能会出现的异常。

    throw

    写在方法内,结束方法。手动抛出异常对象,交给调用者。

    自定义异常

    我们可以自定义一个异常,需要时抛出。同时需要保持继承关系。例如:

    public class NameFormatException extends Exception { }
  • Java入门 学习笔记:Stream 流

    我将继续感谢黑马程序员的 java 课程。

    初爽 Stream 流

    需求:这里有一个放着一些姓名的集合,我需要将姓张的、名字长度为三的姓名放进新集合并打印。

    一般来说,我们可以写两个遍历用 startWith 等方法找到对应条件的姓名并存入新集合。但这很麻烦。

    我们可以直接使用 Stream 流实现一个过滤:

    import java.util.ArrayList;
    
    public class demo {
        public static void main(String[] args) {
            ArrayList<String> list = new ArrayList<>();
            list.add("张三");
            list.add("张无忌");
            list.add("周芷若");
            list.add("赵敏");
            list.add("张三丰");
    
            list.stream().filter(name -> name.startsWith("张")).filter(name -> name.length() == 3).forEach(name -> System.out.println(name));
        }
    }

    输出:

    张无忌
    张三丰

    我们就完成了从“怎么做”到“做什么”的转变。

    注意filter 是惰性操作,只有在遇到 forEach 这样的终端操作时才会真正执行处理。

    概述

    Stream可以由数组或集合创建,对流的操作分为两种:

    1. 中间操作,每次返回一个新的流,可以有多个。(筛选 filter、映射 map、排序 sorted、去重组合 skip—limit)
    2. 终端操作,每个流只能进行一次终端操作,终端操作结束后流无法再次使用。终端操作会产生一个新的集合或值。(遍历 foreach、匹配 find–match、规约 reduce、聚合 max–min–count、收集 collect)

    特性:

    • stream 不存储数据,而是按照特定的规则对数据进行计算,一般会输出结果。
    • stream 不会改变数据源,通常情况下会产生一个新的集合或一个值。
    • stream 具有延迟执行特性,只有调用终端操作时,中间操作才会执行。

    Stream 可以传递数组,但如果数组内的数据是基本数据类型数据,会将数组作为一个元素放入 Stream。所以如果将一个 int[] 放进 stream 再 forEach 遍历,得到的结果只是一个地址值。

    方法

    常用的中间方法

    filter

    用于通过设置的条件过滤出元素。

    List<String>strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
    // 获取空字符串的数量
    long count = strings.stream().filter(string -> string.isEmpty()).count();

    limit

    用于获取指定数量的流。

    Random random = new Random();
    random.ints().limit(10).forEach(System.out::println);

    skip(long n)

    跳过流中的前 n 个元素,返回由剩余元素组成的新流。如果流中元素不足 n 个,则返回空流。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    numbers.stream()
           .skip(2)                // 跳过前2个元素:1,2
           .forEach(System.out::println);
    // 输出:3  4  5

    distinct()

    除流中的重复元素,底层依赖元素的 equals() 和 hashCode() 方法判断是否相同。返回一个去重后的新流。

    • 对于自定义对象,要确保正确重写 equals 和 hashCode,否则去重可能无效。
    • 是一个 有状态 的中间操作(需要记住之前出现过的元素)。
    List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 1, 4);
    numbers.stream()
           .distinct()             // 去重
           .forEach(System.out::println);
    // 输出:1  2  3  4  (顺序保持首次出现的顺序)

    concat(Stream a, Stream b)

    • 两个流必须是同类型的(或子类)。
    • 合并后的流是 按顺序 的:全部 a 元素 → 全部 b 元素
    Stream<String> streamA = Stream.of("A", "B", "C");
    Stream<String> streamB = Stream.of("D", "E");
    Stream<String> result = Stream.concat(streamA, streamB);
    result.forEach(System.out::print);
    // 输出:ABCDE

    map

    将流中的每个元素按照给定的映射函数转换成另一种形式(或提取某个属性),返回一个由转换后元素组成的新流。

    // 将字符串转换成其长度
    List<String> words = Arrays.asList("Java", "Stream", "Map");
    words.stream()
         .map(String::length)
         .forEach(System.out::println);
    // 输出:4  6  3
    
    // 将数字转换成其平方
    Stream.of(1, 2, 3)
          .map(n -> n * n)
          .forEach(System.out::print);   // 输出:149

    常用终结方法

    forEach

    Random random = new Random();
    random.ints().limit(10).forEach(System.out::println);

    count

    返回集合元素个数。

    List<String> list = Arrays.asList("猫", "狗", "鸟");
    long n = list.stream().count();
    System.out.println(n);   // 输出 3

    注意:count() 返回的是 long 类型

    收集(toArray、collect)

    toArray()

    收集流中的数据,并放入数组。

    List<String> list = Arrays.asList("苹果", "香蕉", "橙子");
    String[] arr = list.stream().toArray(String[]::new);
    System.out.println(Arrays.toString(arr));  // [苹果, 香蕉, 橙子]

    对于 toArray()

    • 如果流里面是字符串,String[]::new
    • 如果流里面是整数,Integer[]::new

    collect()

    收集流中的数据,并放入集合。

    List<String> list = Arrays.asList("红", "黄", "蓝", "红");
    List<String> result = list.stream()
                              .filter(s -> s.startsWith("红"))
                              .collect(Collectors.toList());
    System.out.println(result);  // [红, 红]  List 数据可重复
    Set<String> result = list.stream()
                             .filter(s -> s.startsWith("红"))
                             .collect(Collectors.toSet());
    System.out.println(result);  // [红]  只有一个,重复的自动去掉

    注意:用 collect 需要导入 java.util.stream.Collectors

  • Java入门 学习笔记:集合进阶 – 下

    感谢黑马的 Java SE 课程。

    双列集合

    元素成对出现。分为键与值。二者之间是一一对应的关系。

    • 双列集合一次需要存一对数据,分别为键和值
    • 键不能重复,值可以重复
    • 键和值是一一对应的,每一个键只能找到自己对应的值
    • 键 + 值这个整体我们称之为“键值对”或者“键值对对象”,在Java中叫做“Entry对象”

    Map 集合

    Map 集合常见 API

    Map 集合是双列集合的顶层接口。其下有 HashMap、TreeMap。

    Map 集合常用方法

    本章节内容摘自其它页面。【Java基础】Java中Map集合的常用方法_java map的常用方法-CSDN博客

    put

    如果添加的 key 已经存在,则会覆盖原来的 value 值。并返回原值。

    Map<String, Integer> map = new HashMap<>();
    map.put("a", 1);
    map.put("b", 2);
    map.put("a", 3);
    System.out.println(map); // 输出 {a=3, b=2}

    get

    get 方法用于获取指定 key 对应的 value 值,如果 key 不存在,则返回 null。

    Map<String, Integer> map = new HashMap<>();
    map.put("a", 1);
    map.put("b", 2);
    Integer value = map.get("a");
    System.out.println(value); // 输出 1

    remove

    remove 方法用于删除指定 key 对应的键值对。

    Map<String, Integer> map = new HashMap<>();
    map.put("a", 1);
    map.put("b", 2);
    System.out.println(map); // 输出 {a=1, b=2}
    map.remove("a");
    System.out.println(map); // 输出 {b=2}

    clear

    用于清空 Map 中所有的键值对。

    Map<String, Integer> map = new HashMap<>();
    map.put("a", 1);
    map.put("b", 2);
    System.out.println(map); // 输出 {a=1, b=2}
    map.clear();
    System.out.println(map); // 输出 {}

    containsKey & containsValue

    containsKey 和 containsValue 方法分别用于判断 Map 中是否包含指定的 key 或 value 值。

    Map<String, Integer> map = new HashMap<>();
    map.put("a", 1);
    map.put("b", 2);
    boolean hasA = map.containsKey("a");
    boolean hasC = map.containsKey("c");
    boolean hasValue2 = map.containsValue(2);
    boolean hasValue3 = map.containsValue(3);
    System.out.println(hasA); // 输出 true
    System.out.println(hasC); // 输出 false
    System.out.println(hasValue2); // 输出 true
    System.out.println(hasValue3); // 输出 false

    keySet 和 values

    keySet 方法返回 Map 中所有 key 的集合,values 方法返回 Map 中所有 value 的集合。

    Map<String, Integer> map = new HashMap<>();
    map.put("a", 1);
    map.put("b", 2);
    map.put("c", 3);
    Set<String> keySet = map.keySet();
    Collection<Integer> values = map.values();
    System.out.println(keySet); // 输出 [a, b, c]
    System.out.println(values); // 输出 [1, 2, 3]

    size 和 isEmpty

    size 方法返回 Map 中键值对的数量,isEmpty 方法判断 Map 是否为空。

    
    Map<String, Integer> map = new HashMap<>();
    System.out.println(map.size()); // 输出 0
    System.out.println(map.isEmpty()); // 输出 true
    map.put("a", 1);
    System.out.println(map.size()); // 输出 1
    System.out.println(map.isEmpty()); // 输出 false

    Map 集合遍历方式

    1、键找值

    增强 for
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Set;
    
    public class demo {
        public static void main(String[] args) {
            Map<String,String> map = new HashMap<>();
            map.put("zhangsan", "lisi");
            map.put("liwu", "chenliu");
            // 获取所有键并放入一个单列集合
            Set<String> keys = map.keySet();
            for (String key : keys) {
                // 用 get 方法获取其值
                String value = map.get(key);
                System.out.println(key + " = " + value);
            }
        }
    }
    迭代器遍历

    我们同样可以使用迭代器获取每一个键的值。

    import java.util.HashMap;
    import java.util.Iterator;
    import java.util.Map;
    import java.util.Set;
    
    public class demo {
        public static void main(String[] args) {
            Map<String,String> map = new HashMap<>();
            map.put("zhangsan", "lisi");
            map.put("liwu", "chenliu");
            // 获取所有键并放入一个单列集合
            Set<String> keys = map.keySet();
            Iterator<String> iterator = keys.iterator();
            while (iterator.hasNext()) {
                String key = iterator.next();
                String value = map.get(key);
                System.out.println(key + " = " + value);
            }
            // for (String key : keys) {
            //     // 用 get 方法获取其值
            //     String value = map.get(key);
            //     System.out.println(key + " = " + value);
            // }
        }
    }

    2、键值对

    import java.util.HashMap;
    import java.util.Map;
    import java.util.Set;
    
    public class demo {
        public static void main(String[] args) {
            Map<String,String> map = new HashMap<>();
            map.put("zhangsan", "lisi");
            map.put("liwu", "chenliu");
            // 获取所有键并放入一个单列集合
            Set<Map.Entry<String, String>> entries = map.entrySet();
            for (Map.Entry<String,String> entry : entries) {
                String key = entry.getKey();
                String value = entry.getValue();
                System.out.println(key + " = " + value);
            }
        }
    }

    Map.Entry 是 Map 接口内部定义的一个嵌套接口。它表示 Map 中的一个键值对(一个条目)。例如,对于 map.put("zhangsan", "lisi"),这个键值对就是一个 Map.Entry<String, String> 对象。

    HashMap

    Map 的一个实现类。

    • HashMap 是 Map 里面的一个实现类。
    • 没有额外需要学习的特有方法,直接使用 Map 里面的方法就可以了。
    • 特点都是由键决定的:无序、不重复、无索引
    • HashMap 跟 HashSet 底层原理是一模一样的,都是哈希表结构
    • HashMap 底层是哈希表结构的
    • 依赖 hashCode 方法和 equals 方法保证键的唯一
    • 如果键存储的是自定义对象,需要重写 hashCode 和 equals 方法
    • 如果值存储自定义对象,不需要重写 hashCode 和 equals 方法

    LinkedHashMap

    • 由键决定:有序(这里的有序指的是保证存储和取出的元素顺序一致)、不重复、无索引
    • 原理:底层数据结构是依然哈希表,只是每个键值对元素又额外的多了一个双链表的机制记录存储的顺序

    可变参数

    在定义方法时,在最后一个形参后加上三点 ,就表示该形参可以接受多个参数值,多个参数值被当成数组传入。

    • 可变参数只能作为函数的最后一个参数,但其前面可以有也可以没有任何其他参数
    • 由于可变参数必须是最后一个参数,所以一个函数最多只能有一个可变参数
    • Java的可变参数,会被编译器转型为一个数组
    • 变长参数在编译为字节码后,在方法签名中就是以数组形态出现的。这两个方法的签名是一致的,不能作为方法的重载。如果同时出现,是不能编译通过的。可变参数可以兼容数组,反之则不成立

    以上摘自菜鸟教程。Java 可变参数 | 菜鸟教程

    示例:

    public class demo {
        public static void main(String[] args) {
            getSum(1,2,3,4,5);
        }
        
        public static int getSum(int...args) {
            System.out.println(args);// 输出[I@163006a,可以判断此对象是一个数组
            return 0;
        }
    }

    最后一点“变长参数在编译为字节码后,在方法签名中就是以数组形态出现的。这两个方法的签名是一致的,不能作为方法的重载。如果同时出现,是不能编译通过的。可变参数可以兼容数组,反之则不成立”,有点费解,我们拆解一下:

    “变长参数在编译为字节码后,在方法签名中就是以数组形态出现的”

    源码中写的 int... args,编译成 .class 字节码后,方法的参数类型会变成 int[](一维 int 数组)。

    • 可变参数只是语法糖,编译器自动把多个参数包装成数组,方法内部看到的 args 也就是数组。
    • 因此从 JVM 的角度看,根本不存在“可变参数”这种类型,只有数组。

    “这两个方法的签名是一致的,不能作为方法的重载”

    public static int getSum(int... a) { return 0; }
    public static int getSum(int[] b) { return 0; }

    编译时两者的字节码方法签名都是 getSum(int[]),完全一样。

    “如果同时出现,是不能编译通过的”

    void foo(int... x) {}
    void foo(int[] x) {}
    // 编译错误:已在类中定义了方法 foo(int[])

    “可变参数可以兼容数组,反之则不成立”

    • 可变参数可以兼容数组:调用一个声明为 int... 的方法时,你既可以传入多个 int 值(foo(1,2,3)),也可以直接传入一个 int[] 数组(foo(new int[]{1,2,3}))。后者不会报错,数组会被直接当作那个唯一的参数。
    • 反之不成立:如果一个方法声明为 int[](普通数组参数),你不能直接传多个 int 值(foo(1,2,3) 会编译错误),必须显式构造数组再传入。
    public static void acceptVarargs(int... nums) {}
    
    acceptVarargs(1,2,3);        // OK
    acceptVarargs(new int[5]);   // OK  (兼容数组)
    
    public static void acceptArray(int[] arr) {}
    
    acceptArray(1,2,3);          // 编译错误!不能直接传多个值
    acceptArray(new int[]{1,2,3}); // OK

    集合工具类 Collections

    摘于其它页面。Java Collections:专为集合框架而生的工具类 | 二哥的Java进阶之路

    排序

    • reverse(List list):反转顺序
    • shuffle(List list):洗牌,将顺序打乱
    • sort(List list):自然升序
    • sort(List list, Comparator c):按照自定义的比较器排序
    • swap(List list, int i, int j):将 i 和 j 位置的元素交换位置

    示例:

    List<String> list = new ArrayList<>();
    list.add("沉默王二");
    list.add("沉默王三");
    list.add("沉默王四");
    list.add("沉默王五");
    list.add("沉默王六");
    
    System.out.println("原始顺序:" + list);
    
    // 反转
    Collections.reverse(list);
    System.out.println("反转后:" + list);
    
    // 洗牌
    Collections.shuffle(list);
    System.out.println("洗牌后:" + list);
    
    // 自然升序
    Collections.sort(list);
    System.out.println("自然升序后:" + list);
    
    // 交换
    Collections.swap(list, 2,4);
    System.out.println("交换后:" + list);

    输出:

    原始顺序:[沉默王二, 沉默王三, 沉默王四, 沉默王五, 沉默王六]
    反转后:[沉默王六, 沉默王五, 沉默王四, 沉默王三, 沉默王二]
    洗牌后:[沉默王五, 沉默王二, 沉默王六, 沉默王三, 沉默王四]
    自然升序后:[沉默王三, 沉默王二, 沉默王五, 沉默王六, 沉默王四]
    交换后:[沉默王三, 沉默王二, 沉默王四, 沉默王六, 沉默王五]

    查找

    • binarySearch(List list, Object key):二分查找法,前提是 List 已经排序过了
    • max(Collection coll):返回最大元素
    • max(Collection coll, Comparator comp):根据自定义比较器,返回最大元素
    • min(Collection coll):返回最小元素
    • min(Collection coll, Comparator comp):根据自定义比较器,返回最小元素
    • fill(List list, Object obj):使用指定对象填充
    • frequency(Collection c, Object o):返回指定对象出现的次数
    System.out.println("最大元素:" + Collections.max(list));
    System.out.println("最小元素:" + Collections.min(list));
    System.out.println("出现的次数:" + Collections.frequency(list, "沉默王二"));
    
    // 没有排序直接调用二分查找,结果是不确定的
    System.out.println("排序前的二分查找结果:" + Collections.binarySearch(list, "沉默王二"));
    Collections.sort(list);
    // 排序后,查找结果和预期一致
    System.out.println("排序后的二分查找结果:" + Collections.binarySearch(list, "沉默王二"));
    
    Collections.fill(list, "沉默王八");
    System.out.println("填充后的结果:" + list);

    输出:

    原始顺序:[沉默王二, 沉默王三, 沉默王四, 沉默王五, 沉默王六]
    最大元素:沉默王四
    最小元素:沉默王三
    出现的次数:1
    排序前的二分查找结果:0
    排序后的二分查找结果:1
    填充后的结果:[沉默王八, 沉默王八, 沉默王八, 沉默王八, 沉默王八]
  • Java入门 学习笔记:集合进阶 – 上

    前言

    前面学习过了 ArrayList 的基本使用,但依旧有许多需要学习。

    由于这一部分许多知识我现阶段并不会用到,所以直接略过。

    集合分类

    分为单列集合 Collection 与双列 Map 集合。

    字面意思即可理解。单列集合一次添加一个数据,而双列集合一次添加两个相对应的数据。

    集合体系结构

    Collection 分为 List 和 Set,都是接口。

    List 系添加元素有序、可重复、可索引;Set 无序、不重复、不索引。

    Collection 系集合

    Collection 基本操作

    如下示例:

    package test;
    
    import java.util.ArrayList;
    import java.util.Collection;
    
    public class test {
        public static void main(String[] args) {
            Collection<String> coll = new ArrayList<>();
            coll.add("a");
            coll.add("b");
            coll.add("c");
            System.out.println(coll);
    
            coll.remove("a");
            System.out.println(coll);
    
            coll.clear();
            System.out.println(coll);
        }
    }

    应当输出:

    [a, b, c]
    [b, c]
    []

    *contain 方法依赖的 equals 方法默认继承自 Object 类的 equals,比较的是地址值,而不是属性值。

    Collection 通用遍历方法

    迭代器遍历

    迭代器不依赖索引。依靠创建并移动指针来读出所有元素。

    public class collection {
        public static void main(String[] args) {
            // 创建集合,添加元素
            Collection<String> coll = new ArrayList<>();
            coll.add("a");
            coll.add("b");
            coll.add("c");
            coll.add("d");
    
            //获取迭代器对象
            Iterator<String> it = coll.iterator();
            //循环
            while (it.hasNext()) {
                String str = it.next();
                System.out.println(str);
            }
        }
    }

    其它注意事项:

    • 指针不再指向任何元素时调用 next 方法,会报错 NoSuchElementException
    • 遍历结束后,指针不会复位
    • 循环中只能使用一次使用 next 方法

    增强 for 循环(foreach)遍历

    • 增强 for 底层就是迭代器,可以简化书写
    • JDK5 后出现
    • 单列集合和数组才可以增强 for 遍历
    package com.CollctionForEach;
    
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.Iterator;
    
    public class Demo1 {
        public static void main(String[] args) {
            Collection<String> names = new ArrayList<>();
            names.add("张三");
            names.add("李四");
            names.add("王五");
            names.add("赵六");
    
            for (String s : names) {
                System.out.println(s);
            }
        }
    }

    lambda 表达式遍历

    package com.CollctionForEach;
    
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.Iterator;
    
    public class Demo1 {
        public static void main(String[] args) {
            Collection<String> names = new ArrayList<>();
            names.add("张三");
            names.add("李四");
            names.add("王五");
            names.add("赵六");
    
            names.forEach(name -> System.out.println(name));
        }
    }

    List 系集合

    继承了 Collection 方法。因为 Collection 是 List 的超级接口。

    由于 List 集合拥有索引,所以多了很多索引操作。

    List 常用方法:

    • add() – 将元素添加到列表
    • addAll() – 将一个列表的所有元素添加到另一个
    • get() – 有助于从列表中随机访问元素
    • iterator() – 返回迭代器对象,该对象可用于顺序访问列表的元素
    • set() – 更改列表的元素
    • remove() – 从列表中删除一个元素
    • removeAll() – 从列表中删除所有元素
    • clear() – 从列表中删除所有元素(比removeAll()效率更高)
    • size() – 返回列表的长度
    • toArray() – 将列表转换为数组
    • contains() – 如果列表包含指定的元素,则返回true

    add() 方法包含两个参数,一个 index,一个 element。如果索引上已存在数据,则后面所有元素都后移。

    remove() 可以删除指定索引的元素,同时返回删除的元素。

    List 遍历方式

    由于继承了 Collection,所以可以使用上面提到的通用遍历方法。

    多了个索引,就可以用普通 for 循环了:

    for (int i = 0; i < list.size(); i++) {
      String item = list.get(i);
      sout(item);
    }

    数据结构(一)

    后进先出,先进后出。

    队列

    先进先出,后进后出。

    数据从后端进队列的过程,叫作入队列。

    数据从前端出队列的过程,叫作出队列。

    数组

    由于添加、删除元素都需要移动一批元素,所以增删速度慢。

    但是查询速度快,因为元素在内存中是连续存储的。

    链表

    链表种的每一个元素,成为结点,每个结点都是一个独立的对象,拥有自己的地址值。每个结点中,不仅会存储自己的数据,还会存储下一个结点的地址值。与链条结构很相似。

    创建的第一个结点,叫作头结点。

    添加数据:创建新结点,在头节点存储新结点的地址值,即可将结点串联起来。“^”表示空地址值。

    所以,链表的查找速度较慢。数组在内存中是一篇连续的空间,本质上只需要一个数组的地址值,随后就可以使用索引找到元素。链表查找数据则必须从头结点开始查找,一个个向后。

    不过,链表插入数据并不需要像数组那样将一大批的元素后后移,直接修改地址值就可以完成数据的插入。因此,链表的增删速度较快。

    我们可以像每个结点中塞入两个地址值,这样它就可以完成双向查找,效率大幅提升,称为双向链表

    泛型

    概述

    JDK5 引入的新特性,可以在编译阶段约束操作数据的类型,并进行检查。

    格式:<数据类型>。

    泛型只支持引用数据类型

    如果不设定泛型,集合中所有数据类型都是 Object 类。

    泛型的好处:

    • 统一数据类型
    • 将运行时会出现的问题提前至编译阶段检查,可以规避一系列数据类型错误

    Java 中的泛型,是伪泛型。意思就是,这个泛型,只在编译时期有效。

    泛型类

    例如:

    public class ArrayList<E>{
    
    }

    不确定成员数据类型时,可以使用泛型。

    任意字母都可以,比如 Type,Element,Key,Value。

    泛型方法

    一个方法的形参不确定的话,可以在类添加泛型,也可以单独给一个方法加上泛型。例如 public static<E> void method(E e){}

    其它

    泛型本身不可继承,但数据具备继承性。

    数据结构(二)

    二叉树

    由父节点、左/右子节点构成。

    每个节点都是独立的对象,不仅存储当前数据,还会按需存储父节点、左/右子节点的地址值。

    一些术语

    每个节点的子节点数量,叫作度。任意节点度 <= 2 的,叫作二叉树。

    二叉搜索树树的数据规律

    遵守一个规则:小在左,大在右,一样就跳过。

    二叉树的遍历方式

    前序遍历:根结点 —> 左子树 —> 右子树

    中序遍历:左子树—> 根结点 —> 右子树

    后序遍历:左子树 —> 右子树 —> 根结点

    层次遍历:只需按层次遍历即可

    平衡二叉树

    任意节点的左右子树高度差不超过一。

    平衡二叉树的旋转机制

    确定支点:不断向父节点寻找不平衡的节点。

    1. 以不平衡的点为支点
    2. 将支点左旋降级,使其成为左子节点
    3. 晋升原本的右子节点

    右旋同理。

    二叉树需要旋转的四种情况

    一、左左

    当根节点左子树的左子树点有节点插入,导致二叉树不平衡时。

    右旋一次可以保持平衡。

    二、左右

    根节点的左子树的右子树有节点插入。

    先将左子树局部左旋使其成为“左左”的情况,然后右旋即可恢复平衡。

    三、右右

    与“左左相反”。左旋一次恢复平衡。

    四、右左

    同理,局部右旋,随后左旋。

    红黑树

    概述

    • 红黑树是一种自平衡的二叉查找树,是计算机科学中用到的一种数据结构
    • 1972 年出现,当时被称之为平衡二叉B树。后来,1978 年被修改为如今的“红黑树”
    • 它是一种特殊的二叉查找树,红黑树的每一个节点上都有存储位表示节点的颜色
    • 每一个节点可以是红或者黑;红黑树不是高度平衡的,它的平衡是通过”红黑规则”进行实现的

    是一个二叉查找树。

    并不高度平衡,平衡靠红黑规则保证,比二叉树的“任意节点的左右子树高度差不超过一”的要求宽松一些。

    红黑规则

    • 每一个节点或是红色的,或者是黑色的
    • 根节点必须是黑色
    • 如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为Nil,这些Nil视为叶节点,每个叶节点(Nil)是黑色的
    • 如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连的情况)
    • 对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点

    红黑树的节点中,会多存储一个颜色属性,即黑或红。

    添加的节点默认为红色。因为如果添加黑色的话,会违反第五条规则,还需要更换一下颜色。

    Set 类集合

    概述

    • 无序:存取顺序不一致
    • 不重复:可以去除重复
    • 无索引:没有带索引的方法,所以不能使用普通for循环遍历,也不能通过索引来获取元素

    Set 类集合的实现类

    • HashSet:无序、不重复、无索引
    • LinkedHashSet:有序、不重复、无索引
    • TreeSet:可排序、不重复、无索引

    Set 集合的方法与 Collection 方法基本一致。

    HashSet 概述

    没有什么新方法,直接了解一下原理即可。

    不从 0 索引开始向后存储,而是依据一定计算得到整数索引。

    int index = (数组长度 - 1) & HashCode;

    哈希值

    对象的整数表现形式。

    • 根据hashCode方法算出来的int类型的整数
    • 该方法定义在Object类中,所有对象都可以调用,默认使用地址值进行计算
    • 一般情况下,会重写hashCode方法,利用对象内部的属性值计算哈希值

    对象的哈希值属性:

    • 如果没有重写 hashCode 方法,不同对象计算出的哈希值是不同的
    • 如果已经重写 hashCode 方法,不同的对象只要属性值相同,计算出的哈希值就是一样的
    • 在小部分情况下,不同的属性值或者不同的地址值计算出来的哈希值也有可能一样。(哈希碰撞——概率极小,但确有可能)

    我们来演示一下,这里创建两个对象并获取其哈希值。

    public class demo {
        public static void main(String[] args) {
            Student s1 = new Student("zhangsan",23);
            Student s2 = new Student("zhangsan",23);
            System.out.println(s1.hashCode());
            System.out.println(s2.hashCode());
        }
    }

    输出:

    26697217
    3022630

    对于哈希碰撞,我们也可以模拟:

    public class demo {
        public static void main(String[] args) {
            System.out.println("abc".hashCode());
            System.out.println("acD".hashCode());
        }
    }

    输出:

    96354
    96354

    HashSet 底层原理

    • HashSet集合底层采取哈希表存储数据
    • 哈希表是一种对于增删改查数据性能都较好的结构

    Hash 表的组成

    • JDK8之前:数组+链表
    • JDK8开始:数组+链表+红黑树

    模拟一下:

    1. 创建一个默认长度16,默认加载因为0.75的数组,数组名table
    2. 根据元素的哈希值跟数组的长度计算出应存入的位置
    3. 判断当前位置是否为null,如果是null直接存入
    4. 如果位置不为null,表示有元素,则调用equals方法比较属性值
    5. 一样:不存
    6. 不一样:存入数组,形成链表——JDK8以前:新元素存入数组,老元素挂在新元素下面;JDK8以后:新元素直接挂在老元素下面

    同时,当链表长度大于 8 且数组长度大于 64 时,该链表自动转换为红黑树以提高查改效率。

    这时,就会出现三种数据结构:table[] 数组,链表,还有转换后的红黑树。也就是上面说的”数组+链表+红黑树“。

    如果集合中存储的是自定义对象,必须要重写 hashcodeequals 方法。

    关于 HashSet 的三个问题

    问题一:为什么 HashSet 的存取的顺序不一样?

    既然索引是计算而来,就有可能出现后添加的数据存储在前边的情况了。

    问题二:为什么 HashSet 没有索引?

    首先,HashSet 是不重复的(HashCodeequals 方法)。

    而且,有索引的是数组,链表会挂许多元素。我们不能够根据链表头结点所在的索引去找到链表里的元素。所以,是不索引的。

    问题三:如何保证去重?

    HashCodeequals 方法。

    一样的会跳过不存。不存在重复数据的情况。

    综上,我们书写如下代码:

    import java.util.HashSet;
    
    public class demo {
        public static void main(String[] args) {
            Student s1 = new Student("zhangsan",23);
            Student s2 = new Student("lisi",24);
            Student s3 = new Student("wangwu",25);
            Student s4 = new Student("zhangsan",23);
    
            HashSet<Student> hs = new HashSet<>();
            System.out.println(hs.add(s1));
            System.out.println(hs.add(s2));
            System.out.println(hs.add(s3));
            System.out.println(hs.add(s4));
    
            System.out.println(hs);
        }
    }
    
    // 输出:
    // true
    // true
    // true
    // false
    // [Student@42ca5bd9, Student@62365a6, Student@a8e9e1e4]
    // 打印地址值是因为没有重写 toString

    并且在 Student 类中重写 hashCode() 和 equals(),基于 name 和 age 来定义相等的逻辑。不然会返回四个 true

        @Override
        public int hashCode() {
            return Objects.hash(name, age);
        }
    
        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null || getClass() != obj.getClass()) return false;
            Student student = (Student) obj;
            return age == student.age && Objects.equals(name, student.name);
        }

    LinkedHashSet 概述

    • 有序、不重复、无索引。
    • 这里的有序指的是保证存储和取出的元素顺序一致
    • 原理:底层数据结构是依然哈希表,只是每个元素又额外的多了一个双链表的机制记录存储的顺序。

    每一个结点都会同相邻存储的数据建立一个双向链表。这样,取出时,顺序就是一样的了。

    由于我们需要观察去除顺序,这里顺手重写一下 Student 类的 toString 方法:

    public String toString () { return "Student{name =" + name + ", age =" + age + "}"; }
    import java.util.LinkedHashSet;
    
    public class demo {
        public static void main(String[] args) {
            Student s1 = new Student("zhangsan",23);
            Student s2 = new Student("lisi",24);
            Student s3 = new Student("wangwu",25);
            Student s4 = new Student("zhangsan",23);
    
            LinkedHashSet<Student> hs = new LinkedHashSet<>();
            System.out.println(hs.add(s1));
            System.out.println(hs.add(s2));
            System.out.println(hs.add(s3));
            System.out.println(hs.add(s4));
    
            System.out.println(hs);
        }
    }
    // input:
    // true
    // true
    // true
    // false
    // [Student{name =zhangsan, age =23}, Student{name =lisi, age =24}, Student{name =wangwu, age =25}]

    可以观察到,数据的取出确实是有序的。

    TreeSet 概述

    • 不重复、无索引、可排序
    • 可排序:按照元素的默认规则(有小到大)排序
    • TreeSet集合底层是基于红黑树的数据结构实现排序的,增删改查性能都较好
    // import java.util.LinkedHashSet;
    
    import java.util.TreeSet;
    
    public class demo {
        public static void main(String[] args) {
            // Student s1 = new Student("zhangsan",23);
            // Student s2 = new Student("lisi",24);
            // Student s3 = new Student("wangwu",25);
            // Student s4 = new Student("zhangsan",23);
    
            TreeSet<Integer> hs = new TreeSet<>();
            hs.add(5);
            hs.add(2);
            hs.add(3);
            hs.add(4);
            hs.add(1);
            System.out.println(hs);
        }
    }
    // 输出:
    // [1, 2, 3, 4, 5]
    // 这是因为 TreeSet 默认从小到大自动排序

    TreeSet 集合默认的规则

    • 对于数值类型:IntegerDouble,默认按照从小到大的顺序进行排序。
    • 对于字符、字符串类型:按照字符在ASCII码表中的数字升序进行排序。

    TreeSet 比较器排序

    Student 类:

    public class Student {
    	private String name;
    	private int age;
    	
    	public Student(String name, int age) {
    		super();
    		this.name = name;
    		this.age = age;
    	}
    	
    	@Override
    	public String toString() {
    		return "Student [name=" + name + ", age=" + age + "]";
    	}
    }

    测试类:

    public class DemoTreeSet {
    	public static void main(String[] args) {
    		TreeSet<Student> ts = new TreeSet<>();
    		ts.add(new Student("张廷玉",43));
    		ts.add(new Student("纳兰明珠",80));
    		ts.add(new Student("索额图",79));
    		System.out.println(ts);
    	}
    }

    如果没有自定义比较器,TreeSet 集合存储的对象元素必须实现 Comparable 接口。这里我们的 Student 类既没有定义比较器,没有实现 Comparable 接口,所以会报 java.lang.ClassCastException 异常,意思就是这里存的元素不是 Comparable 类型的。

    实现 Comparable 接口:

    	@Override
    	public int compareTo(Student o) {
    		return this.age - o.age;
    	}

    按照这个规则,就可以实现依照 age 属性进行升序排序了。

  • Java 入门 学习笔记:lambda 表达式

    初试 lambda 表达式

    代码示例:

    // 传统的匿名内部类
    Runnable runnable1 = new Runnable() {
        @Override
        public void run() {
            System.out.println("Hello World!");
        }
    };
    
    // 使用 Lambda 表达式
    Runnable runnable2 = () -> System.out.println("Hello World!");

    可以看到,lambda 表达式表示的代码更加简洁了。

    函数式编程

    一种编程思想特点。

    lambda 表达式使用前提

    必须是接口的匿名内部类,接口中只能有一个抽象方法。

    对于只有一个抽象方法的抽象接口,可以加上 @FunctionalInterface 标签。编译器会确认该接口是否只有一个抽象方法。若出现多个方法则编译报错。

    ——正是因为你的接口只有一个抽象方法,所以你的 lambda 表达式才可以省略掉重写方法名。

    示例

    原代码:

    public class test {
        public static void main(String[] args) {
            method(new Swim() {
                @Override
                public void swimming() {
                    System.out.println("正在游泳");
                }
            });
        }
    
        public static void method(Swim s) {
            s.swimming();
        }
    
        public interface Swim {
            public abstract void swimming();
        }
    }

    改写后:

    public class test {
        public static void main(String[] args) {
            method(() -> {
                    System.out.println("正在游泳");
                }
            );
        }
    
        public static void method(Swim s) {
            s.swimming();
        }
    
        public interface Swim {
            public abstract void swimming();
        }
    }

    lambda 表达式省略

    原则:省略内容可推导。

    1. 参数类型可以省略不写
    2. 如果只有一个参数,不仅参数类型,() 也可以省略不写
    3. 如果表达式的方法体只有一行,大括号,分号,return 都可以省略不写。但必须同时省略。

    依照如上原则,我们的代码还可以省略成下面的形式:

    method(() -> System.out.println("Swimming"));

    小练习

    将一个数组内的字符串按照从短到长的顺序排列。

    Arrays.sort(arr, ) 方法有两个形参,一个是传入数组,一个是规则模式。我们这里使用比较模式。

    import java.util.Arrays;
    import java.util.Comparator;
    
    public class Lambda {
        public static void main(String[] args) {
            String[] arr = {"a", "aaaa", "aaa", "aa"};
    
            // 完整的匿名内部类写法:
            // Arrays.sort(arr, new Comparator<String>(){
            //     @Override
            //     public int compare(String o1, String o2) {
            //         return o1.length() - o2.length();
            //     }
            // });
    
            Arrays.sort(arr, (o1, o2) -> o1.length() - o2.length());
            
            System.out.println(Arrays.toString(arr));
        }
    }

  • Java 入门 学习笔记:一些常见算法

    感谢黑马程序员的免费课程!大爱!

    查找算法

    基本查找

    从 0 索引开始遍历查找。

    public class test {
        public static void main(String[] args) {
            int[] arr = {131, 127, 147, 81, 103, 23, 7, 79};
            int number = 82;
            System.out.println(basicSearch(arr, number));
        }
        
        public static boolean basicSearch(int[] arr, int number){
            for (int i = 0; i < arr.length; i++) {
                if(arr[i] == number){
                    return true;
                }
            }
            return false;
        }
    

    二分/折半查找

    只能查找有序元素。

    每次都将元素分成两半,如果大了,就往小的范围移动 1,如果小了则反之。所以需要有序元素。

    public class test {
        public static void main(String[] args) {
            int[] arr = {7, 23, 79, 81, 103, 127, 131, 147};
            System.out.println(binarySearch(arr, 150));
        }
        public static int binarySearch(int[] arr, int number){
            int min = 0;
            int max = arr.length - 1;
    
            while(true){
                if(min > max){
                    return -1;
                }
                int mid = (min + max) / 2;
                if(arr[mid] > number){
                    max = mid - 1;
                }else if(arr[mid] < number){
                    min = mid + 1;
                }else{
                    return mid;
                }
            }
        }
    }

    计算公式如下:

    pos=low+(keyarrlow)(arrhigharrlow)×(highlow)pos = low + \frac{(key – arr_{\text{low}})}{(arr_{\text{high}} – arr_{\text{low}})} \times (high – low)
    • low:当前查找区间的下界
    • high:当前查找区间的上界
    • key:要查找的值
    • arr[low] 和 arr[high] 分别是区间的最小值和最大值

    这个公式会在一段线性分布的数据内给出越来越精确的估算位置,直到 pos 位置的数据等于 key。

    分块查找

    <!--复制自黑马程序员的资料-->

    当数据表中的数据元素很多时,可以采用分块查找。

    汲取了顺序查找和折半查找各自的优点,既有动态结构,又适于快速查找,分块查找适用于数据较多,但是数据不会发生变化的情况,如果需要一边添加一边查找,建议使用哈希查找

    1. 需要把数据分成N多小块,块与块之间不能有数据重复的交集。
    2. 给每一块创建对象单独存储到数组当中
    3. 查找数据的时候,先在数组查,当前数据属于哪一块
    4. 再到这一块中顺序查找

    分块的原则1:前一块中的最大数据,小于后一块中所有的数据(块内无序,块间有序)
    分块的原则2:块数数量一般等于数字的个数开根号。比如:16个数字一般分为4块左右。
    核心思路:先确定要查找的元素在哪一块,然后在块内挨个查找。

    public class A03_BlockSearchDemo {
        public static void main(String[] args) {
            /*
                分块查找
                核心思想:
                    块内无序,块间有序
                实现步骤:
                    1.创建数组blockArr存放每一个块对象的信息
                    2.先查找blockArr确定要查找的数据属于哪一块
                    3.再单独遍历这一块数据即可
            */
            int[] arr = {16, 5, 9, 12,21, 18,
                         32, 23, 37, 26, 45, 34,
                         50, 48, 61, 52, 73, 66};
     
            //创建三个块的对象
            Block b1 = new Block(21,0,5);
            Block b2 = new Block(45,6,11);
            Block b3 = new Block(73,12,17);
     
            //定义数组用来管理三个块的对象(索引表)
            Block[] blockArr = {b1,b2,b3};
     
            //定义一个变量用来记录要查找的元素
            int number = 37;
     
            //调用方法,传递索引表,数组,要查找的元素
            int index = getIndex(blockArr,arr,number);
     
            //打印一下
            System.out.println(index);
        }
     
        //利用分块查找的原理,查询number的索引
        private static int getIndex(Block[] blockArr, int[] arr, int number) {
            //1.确定number是在那一块当中
            int indexBlock = findIndexBlock(blockArr, number);
     
            if(indexBlock == -1){
                //表示number不在数组当中
                return -1;
            }
     
            //2.获取这一块的起始索引和结束索引   --- 30
            // Block b1 = new Block(21,0,5);   ----  0
            // Block b2 = new Block(45,6,11);  ----  1
            // Block b3 = new Block(73,12,17); ----  2
            int startIndex = blockArr[indexBlock].getStartIndex();
            int endIndex = blockArr[indexBlock].getEndIndex();
     
            //3.遍历
            for (int i = startIndex; i <= endIndex; i++) {
                if(arr[i] == number){
                    return i;
                }
            }
            return -1;
        }
     
        //定义一个方法,用来确定number在哪一块当中
        public static int findIndexBlock(Block[] blockArr,int number){ //100
     
            //从0索引开始遍历blockArr,如果number小于max,那么就表示number是在这一块当中的
            for (int i = 0; i < blockArr.length; i++) {
                if(number <= blockArr[i].getMax()){
                    return i;
                }
            }
            return -1;
        }
    }
     
    class Block{
        private int max;//最大值
        private int startIndex;//起始索引
        private int endIndex;//结束索引
        //构造方法,set/get方法
        public String toString() {
            return "Block{max = " + max + ", startIndex = " + startIndex + ", endIndex = " + endIndex + "}";
        }
    }
    

    哈希查找

    先计算出当前数据的哈希值,用哈希值跟数组的长度进行计算,计算出应存入的位置,再挂在数组的后面形成链表,如果挂的元素太多而且数组长度过长,也会把链表转化为红黑树,进一步提高效率。

    排序算法

    冒泡排序

    将相邻数据两两比较,小的放前面,大的放后面。(举例,也可按需更改)

    public class BubbleSort {
        public static void bubbleSort(int[] arr) {
            int n = arr.length;
            // 外层循环:需要 n-1 轮
            for (int i = 0; i < n - 1; i++) {
                // 内层循环:相邻比较,每轮比较次数减少
                for (int j = 0; j < n - 1 - i; j++) {
                    if (arr[j] > arr[j + 1]) {
                        // 交换
                        int temp = arr[j];
                        arr[j] = arr[j + 1];
                        arr[j + 1] = temp;
                    }
                }
            }
        }
    
        public static void main(String[] args) {
            int[] arr = {5, 2, 9, 1, 5};
            bubbleSort(arr);
            for (int num : arr) {
                System.out.print(num + " ");
            }
        }
    }

    选择排序

    每次从待排序的序列中选出最小(或最大)的元素,放到已排序序列的末尾。

    数组:[5, 2, 9, 1, 5]

    第一轮(找最小,放到索引0)

    • 扫描整个数组 [5, 2, 9, 1, 5],最小值为 1(索引3)
    • 交换索引0和索引3 → [1, 2, 9, 5, 5]
      (此时 1 已在正确位置)

    第二轮(找第二小,放到索引1)

    • 扫描剩余部分 [2, 9, 5, 5],最小值 2(索引1本身)
    • 不用交换 → [1, 2, 9, 5, 5]

    第三轮(找第三小,放到索引2)

    • 扫描 [9, 5, 5],最小值 5(索引3)
    • 交换索引2和索引3 → [1, 2, 5, 9, 5]

    第四轮(找第四小,放到索引3)

    • 扫描 [9, 5],最小值 5(索引4)
    • 交换索引3和索引4 → [1, 2, 5, 5, 9]

    排序完成。

    public class SelectionSort {
        public static void selectionSort(int[] arr) {
            int n = arr.length;
            // 外层循环:每轮确定一个位置(0 到 n-2)
            for (int i = 0; i < n - 1; i++) {
                // 假设当前位置 i 的元素是最小值
                int minIndex = i;
                // 内层循环:在 i+1 到末尾找真正的最小值索引
                for (int j = i + 1; j < n; j++) {
                    if (arr[j] < arr[minIndex]) {
                        minIndex = j;
                    }
                }
                // 如果最小值不在 i 位置,交换
                if (minIndex != i) {
                    int temp = arr[i];
                    arr[i] = arr[minIndex];
                    arr[minIndex] = temp;
                }
            }
        }
    
        public static void main(String[] args) {
            int[] arr = {5, 2, 9, 1, 5};
            selectionSort(arr);
            for (int num : arr) {
                System.out.print(num + " ");
            }
        }
    }

    插入排序

    将 0 索引到 N 索引的数据认为是有序的,其余数据认为是无序的。

    随后,遍历无序部分的数据,将其插入到有序部分中适当的位置。遇到相同的数据则放在后面。

    public class InsertSort {
        public static void main(String[] args) {
            int[] array = new int[]{15,63,97,12,235,66};
            //控制拿去每一个元素
            for(int i=1;i<array.length;i++){
                //比较次数
                for (int j=i;j>=1;j--){
                    //是否小于前面的元素
                    if (array[j]<array[j-1]){
                        int temp = 0;
                        temp = array[j];
                        array[j] = array[j-1];
                        array[j-1] = temp;
                    }else {
                        //continue 与 break
                        break;
                    }
                }
            }
            System.out.println("排序后的结果:"+ Arrays.toString(array));
        }
    }

    递归算法

    方法中调用方法本身的现象。

    如果我们直接在方法中调用方法本身,例如:

    public class test {
        public static void main(String[] args) {
            method();
        }
    
        public static void method() {
            method();
        }
    }

    会发生栈内存溢出错误。

    所以,递归一定要有出口,否则就会出现内存溢出。

    例如,我们利用递归求 1-100 之间的和。

    public class test {
        public static void main(String[] args) {
            System.out.println(getSum(100));
        }
    
        public static int getSum(int number) {
            if(number == 1) {
                return 1;
            }
            return number + getSum(number - 1);
        }
    }

    这就实现了把一个大问题拆解成多个小问题来解决。同时,程序会在 number == 1 时找到出口,所以不发生内存溢出错误。

    快速排序

    快速排序(Quick Sort)是从冒泡排序算法演变而来的,实际上是在冒泡排序基础上的递归分治法。快速排序在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成了两个部分。

    先以 0 索引的数据作为基准数,随后设置两个指针,这里将起始位置叫作 start,终止位置叫作 end。我们将 end 同基准数比较,若大于基准数,不操作,随后 end 索引 — 至上一个数。如果大于基准数,重复操作;如果小于基准数,则开始将 start 指针向右移动。重复。当 start 索引对应数据大于基准数时,将 start 与 end 索引对应数据交换。

    重复以上操作。

    多次交换,直到 start 与 end 指向同一个索引时,这个索引便是基准数在数组应当存入的位置。也就是基准数归位。将基准数与这个索引对应的数据交换。

    现在,这个基准数将数组分为了两个部分。我们继续在左、右两边重复上述操作。

    然后我们就可以应用递归的思想,不断将左右两部分再分别分为左右两部分。

    代码示例:

    public class quicksort {
        public static void main(String[] args) {
            int[] arr = {6, 1, 2, 7, 9, 3, 12, 4, 5, 10 ,8 ,11};
            QuickSort(arr, 0, arr.length - 1);
            for (int i = 0; i < arr.length; i ++) {
                System.out.print(arr[i] + " ");
            }
        }
    
        public static void QuickSort(int[] arr, int i, int j) {
            // 定义两个变量用于存储两个指针
            int start = i;
            int end = j;
    
            // 递归需要一个出口
            // 如果 start 大于 end,就说明这一部分是空区间,我们直接 return
            if (start > end) {
                return;
            }
    
            // 记录基准数
            int baseNumber = arr[i];
            
            while (start != end) {
                while (true) {
                    if (end <= start || end < baseNumber) {
                        break;
                    }
                    end --;
                }
                
                while (true) {
                    if (end <= start || end > baseNumber) {
                        break;
                    }
                    start ++;          
                }
    
                int temp = arr[start];
                arr[start] = arr[end];
                arr[end] = temp;
            }
            // 基准数归位
            int temp = arr[i];
            arr[i] = arr[start];
            arr[start] = temp;
    
            // 确定左右两部分的范围并重复操作,也就是递归的思想
            QuickSort(arr, i, start - 1);
            QuickSort(arr, start +1, j);
        }
    }

    快速排序排序一百万个数据,也只需要几百毫秒的时间。效率极高。