一、Java 面向对象思想
- 继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类、基类);得到
继承信息的类被称为子类(派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因
素的重要手段。
封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。面向对象
的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我
们编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程
接口。多态性:多态性是指允许不同子类型的对象对同一消息作出不同的响应。简单的说就是用同样的对象引用调
用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。如果将对象的方法视为对象向外
界提供的服务,那么运行时的多态性可以解释为:当 A 系统访问 B 系统提供的服务时,B 系统有多种提供服务的方式,
但一切对 A 系统来说都是透明的。方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写
(override)实现的是运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的东西,要实现多态需要
做两件事:1. 方法重写(子类继承父类并重写父类中已有的或抽象的方法);2. 对象造型(用父类型引用引用子类型
对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注
对象有哪些属性和行为,并不关注这些行为的细节是什么。
二、Java 中的多态
- Java 中实现多态的机制是什么?
靠的是父类或接口定义的引用变量可以指向子类或具体实现类的实例对象,而程序调用的方法在运行期才动
态绑定,就是引用变量所指向的具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变
量的类型中定义的方法。
三、Java 的异常处理
Java 中异常分为哪些种类
按照异常需要处理的时机分为编译时异常也叫CheckedException和运行时异常也叫RuntimeException。
只有java语言提供了Checked异常,Java认为Checked异常都是可以被处理的异常,所以Java程序必须显式处理Checked异常。
如果程序没有处理Checked异常,该程序在编译时就会发生错误无法编译。
这体现了Java的设计哲学:没有完善错误处理的代码根本没有机会被执行。对Checked异常处理方法有两种:
- 当前方法知道如何处理该异常,则用 try…catch 块来处理该异常。
- 当前方法不知道如何处理,则在定义该方法是声明抛出该异常。
运行时异常只有当代码在运行时才发行的异常,编译时不需要 try catch。Runtime 如除数是 0 和数组下标越
界等,其产生频繁,处理麻烦,若显示申明或者捕获将会对程序的可读性和运行效率影响很大。所以由系统自动
检测并将它们交给缺省的异常处理程序。当然如果你有处理要求也可以显示捕获它们。调用下面的方法,得到的返回值是什么
1 | public int getNum(){ |
代码在走到第3行的时候遇到了一个MathException,这时第四行的代码就不会执行了,代码直接跳转到catch语句中,走到第6行的时候,
异常机制有这么一个原则如果在catch中遇到了return或者异常等能使该函数终止的话那么用finally就必须先执行完finally代码块里面的代码然后再返回值。
因此代码又跳到第8行,可惜第8行是一个return语句,那么这个时候方法就结束了,因此第6行的返回结果就无法被真正返回。
如果finally仅仅是处理了一个释放资源的操作,那么该道题最终返回的结果就是2。因此上面返回值是3
四、Java 的数据类型
Java 的基本数据类型都有哪些各占几个字节
Java 有 8 种基本数据类型
byte 1
char 2
short 2
int 4
float 4
double 8
long 8
boolean 1(boolean 类型比较特别可能只占一个 bit,多个 boolean 可能共同占用一个字节)String 是基本数据类型吗?可以被继承吗?
String 是引用类型,底层用 char 数组实现的。因为 String 是 final 类,在 java 中被 final 修饰的类不能被继承,
因此 String 当然不可以被继承。
五、Java 的 IO
- Java 中有几种类型的流
字节流和字符流。字节流继承于 InputStream 和 OutputStream,字符流继承于 InputStreamReader 和
OutputStreamWriter - 字节流如何转为字符流
字节输入流转字符输入流通过 InputStreamReader 实现,该类的构造函数可以传入 InputStream 对象。
字节输出流转字符输出流通过 OutputStreamWriter 实现,该类的构造函数可以传入 OutputStream 对象。
1 | //对象输出流 |
- 如何将一个 java 对象序列化到文件里
在 java 中能够被序列化的类必须先实现 Serializable 接口,该接口没有任何抽象方法只是起到一个标记作用。
六、Java 的集合
- HashMap 排序题,上机题
已知一个HashMap<Integer,User>集合,User有name(String)和age(int)属性。请写一个方法实现对HashMap的排序功能,
该方法接收HashMap<Integer,User>为形参,返回类型为HashMap<Integer,User>,要求对HashMap中的User的age倒序进行排序。
排序时key=value键值对不得拆散。:要做出这道题必须对集合的体系结构非常的熟悉。HashMap本身就是不可排序的,但是该道题偏偏让给HashMap排序,
那我们就得想在API中有没有这样的Map结构是有序的,LinkedHashMap,对的,就是他,他是Map结构,也是链表结构,有序的,
更可喜的是他是HashMap的子类,我们返回LinkedHashMap<Integer,User>即可,还符合面向接口(父类编程的思想)。
但凡是对集合的操作,我们应该保持一个原则就是能用JDK中的API就有JDK中的API,比如排序算法我们不应该去用冒泡或者选择,而是首先想到用Collections集合工具类。
1 | public class HashMapTest { |
- 集合的安全性问题
请问ArrayList、HashSet、HashMap是线程安全的吗?如果不是我想要线程安全的集合怎么办?我们都看过上面那些集合的源码(如果没有那就看看吧),
每个方法都没有加锁,显然都是线程不安全的。话又说过来如果他们安全了也就没第二问了。在集合中Vector和HashTable倒是线程安全的。
你打开源码会发现其实就是把各自核心方法添加上了synchronized关键字。Collections工具类提供了相关的API,可以让上面那3个不安全的集合变为安全的。
1 | // Collections.synchronizedCollection(c) |
上面几个函数都有对应的返回值类型,传入什么类型返回什么类型。打开源码其实实现原理非常简单,就是将集
合的核心方法添加上了 synchronized 关键字。
七、Java 的多线程
多线程的两种创建方式
java.lang.Thread类的实例就是一个线程但是它需要调用java.lang.Runnable接口来执行,
由于线程类本身就是调用的Runnable接口所以你可以继承java.lang.Thread类或者直接实现Runnable接口来重写run()方法实现线程。在 java 中 wait 和 sleep 方法的不同?
最大的不同是在等待时wait会释放锁,而sleep一直持有锁。wait通常被用于线程间交互,sleep通常被用于暂停执行。synchronized 和 volatile 关键字的作用
一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是
立即可见的。
2)禁止进行指令重排序。
volatile 本质是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。- volatile 仅能使用在变量级别;
synchronized 则可以使用在变量、方法、和类级别的 - volatile 仅能实现变量的修改可见性,并不能保证原子性;
synchronized 则可以保证变量的修改可见性和原子性 - volatile 不会造成线程的阻塞;
synchronized 可能会造成线程的阻塞。 - volatile 标记的变量不会被编译器优化;
synchronized 标记的变量可以被编译器优化
- volatile 仅能使用在变量级别;
分析线程并发访问代码解释原因
1 | public class Counter { |
在线字符串去空格是一个用js写的小工具上面的代码执行完后输出的结果确定为1000吗?答案是不一定,或者不等于1000。
这是为什么吗?在java的内存模型中每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。
当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线
程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,
在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象
的值就产生变化了。
也就是说上面主函数中开启了 1000 个子线程,每个线程都有一个变量副本,每个线程修改变量只是临时修改了
自己的副本,当线程结束时再将修改的值写入在主内存中,这样就出现了线程安全问题。因此结果就不可能等于 1000
了,一般都会小于 1000。
上面的解释用一张图表示如下:

- 什么是线程池,如何使用?
线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用new线程而是直接去池中拿线程即可,
节省了开辟子线程的时间,提高的代码执行效率。
在JDK的java.util.concurrent.Executors中提供了生成多种线程池的静态方法。
然后调用他们的 execute 方法即可。
1 | ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); |
请叙述一下您对线程池的理解?
(如果问到了这样的问题,可以展开的说一下线程池如何用、线程池的好处、线程池的启动策略)
合理利用线程池能够带来三个好处。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定
性,使用线程池可以进行统一的分配,调优和监控。线程池的启动策略?
1、线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
2、当调用execute()方法添加一个任务时,线程池会做如下判断:
a.如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
b.如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列。
c.如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建线程运行这个任务;
d.如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。
3、当一个线程完成任务时,它会从队列中取下一个任务来执行。4、当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。三个线程 a、b、c 并发运行,b,c 需要 a 线程的数据怎么实现
根据问题的描述,我将问题用以下代码演示,ThreadA、ThreadB、ThreadC,ThreadA用于初始化数据num,
只有当num初始化完成之后再让ThreadB和ThreadC获取到初始化后的变量num。
分析过程如下:考虑到多线程的不确定性,因此我们不能确保ThreadA就一定先于ThreadB和ThreadC前执行,
就算ThreadA先执行了,我们也无法保证ThreadA什么时候才能将变量num给初始化完成。
因此我们必须让ThreadB和ThreadC去等待ThreadA完成任何后发出的消息。现在需要解决两个难题,
一是让ThreadB和ThreadC等待ThreadA先执行完,二是ThreadA执行完之后给ThreadB和ThreadC发送消息。
解决上面的难题我能想到的两种方案,
一是使用纯JavaAPI的Semaphore类来控制线程的等待和释放,
二是使用Android提供的Handler消息机制。
1 | public class ThreadCommunication { |
八、JavaSE 高级
一、Java 中的反射
- 说说你对 Java 中反射的理解
Java中的反射首先是能够获取到Java中要反射类的字节码,获取字节码有三种方法,- Class.forName(className)
- 类名.class3.this.getClass()。然后将字节码中的方法,变量,构造函数等映射成相应的Method、Filed、Constructor等类,这些类提供了丰富的方法可以被我们所使用。
二、Java 中的动态代理
- 写一个 ArrayList 的动态代理类(笔试题)
1 | final List<String> list = new ArrayList<String>(); |
动静态代理的区别,什么场景使用?
静态代理通常只代理一个类,动态代理是代理一个接口下的多个实现类。静态代理事先知道要代理的是什么,
而动态代理不知道要代理什么东西,只有在运行时才知道。动态代理是实现JDK里的InvocationHandler接口的invoke方法,
但注意的是代理的是接口,也就是你的业务类必须要实现接口,通过Proxy里的newProxyInstance得到代理对象。
还有一种动态代理CGLIB,代理的是类,不需要业务类继承接口,通过派生的子类来实现代理。
通过在运行时,动态修改字节码达到修改类的目的。AOP编程就是基于动态代理实现的,比如著名的Spring框架、Hibernate框架等等都是动态代理的使用例子。
三、Java 中的设计模式
你所知道的设计模式有哪些
Java中一般认为有23种设计模式,我们不需要所有的都会,但是其中常用的几种设计模式应该去掌握。
下面列出了所有的设计模式。需要掌握的设计模式我单独列出来了,当然能掌握的越多越好。
总体来说设计模式分为三大类:
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。单例设计模式
- 最好理解的一种设计模式,分为懒汉式和饿汉式。
饿汉式:
- 最好理解的一种设计模式,分为懒汉式和饿汉式。
1 | public class Singleton { |
懒汉式:
1 | public class Singleton { |
四、Java 的类加载器
1. Java 的类加载器的种类都有哪些?
1. 根类加载器(Bootstrap) --C++写的 ,看不到源码
2. 扩展类加载器(Extension) --加载位置 :jre\lib\ext 中
3. 系统(应用)类加载器(System\App) --加载位置 :classpath 中
4. 自定义加载器(必须继承 ClassLoader)
2. 类什么时候被初始化?
1. 创建类的实例,也就是 new 一个对象
2. 访问某个类或接口的静态变量,或者对该静态变量赋值
3. 调用类的静态方法
4. 反射(Class.forName("com.lyj.load"))
5. 初始化一个类的子类(会首先初始化子类的父类)
6. JVM 启动时标明的启动类,即文件名和类名相同的那个类
只有这 6 中情况才会导致类的类的初始化。
类的初始化步骤:
1. 如果这个类还没有被加载和链接,那先进行加载和链接
2. 假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)
3. 加入类中存在初始化语句(如 static 变量和 static 块),那就依次执行这些初始化语句。
- 本文链接: https://blog.hansong.icu/2021/03/06/Android_Java/
- 版权声明: 本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 4.0 许可协议。