文章目录
一、面向对象三大特征
1.封装 2.继承 3.多态
注:如果问四大特征,则额外添加一个特征:“抽象”。
封装:函数的封装是一种形式,隐藏对象的属性和实现细节,仅仅对外提供函数的接口和对象进行交互。
类的访问限定符可以协助其完成封装
二、 访问限定符
访问权限:public > protected > default > private
三、多态的实现原理
1.静态绑定:重载
2.动态绑定:重写
四、重载与重写的区别
重载条件:相同作用域,方法名相同,参数列表不同。
重写条件:父子类继承关系,子类重写父类方法(方法名相同,参数列表相同,子类修饰符权限>=父类修饰符权限)
五、final关键字
final—终极,即为不可更改的
final修饰变量
final int INTSIZE = 10;
此时INTSIZE为常量
final修饰方法
final void fun() { }
此时fun()方法不允许重写
final修饰类
final class String{ }
此时String类不能被继承
六、static关键字
1.static关键字的作用
-
变量
静态变量和实例变量的区别:静态变量存储在方法区;实例变量存储在堆。
静态变量和类有关,一个类只有一份静态变量;实例变量和对象有关,几个对象几份实例变量。 -
方法
静态方法调用方式:类名.静态方法; 实例方法调用方式:对象.实例方法。
静态方法不能被重写,private方法不能被重写。 -
类
静态只能修饰内部类
静态内部类和实例内部类的区别:是否包含外部类对象的this
静态内部类:
class SingleLink{
private Node head;
class Node{
private int value;
public void fun(){
SingleLink.this.head = null;
}
}
}
实例内部类:
class SingleLink{
private Node head;
static class Node{
private int value;
public void fun(){
//head = null; error 静态不含SingleLink.this
}
}
}
2.单例模式
3.类加载
七、单例模式
条件:构造函数私有,公有静态函数返回实例对象
1. 饿汉单例
线程安全,安全是由类加载机制保证
public class Single1{
private static final Single1 s = new Single1();
private Single1(){ }
public static Single1 getInstance(){
return s;
}
public static void main(String[] args){
//类加载过程
//7步 static -使用-销毁对象
Single1 single1 = Single1.getInstance();
}
}
2. 懒汉单例
懒汉单例模式和饿汉单例模式的区别就是:懒汉创建了延迟对象,同时饿汉单例对象被修饰为final类型。
优点:尽最大可能节省内存空间
缺点:在多线程编程中,使用懒汉式可能造成类的对象在内存中不唯一,虽然用过修改代码可以改正这些问题,但是降低了效率
线程不安全
public class Single2{
private static Single2 s = null;
private Single2(){ }
public static Single2 getInstance(){
//两个线程同时进行if(s == null)判断,则都会进入if条件,就会创建多个实例
if(s == null)
s = new Single2();
return s;
}
}
八、类加载
1.过程
装载(加载) -> 链接 -> 初始化
加载产物:class对象,用于保存当前类的类信息,class对象是方法区的入口。
2.条件
- new对象
- main函数类
- 类名.静态成员
- 反射:
People.class
Class.forName(“People”)
九、接口和抽象类的区别
1. 应用场景
- 抽象类(work),一种事物(People)的抽象: People work()
- 接口,一种行为的抽象:包含抽象方法,常量
例:ArrayList,LinkedList都实现了List接口,List接口则实现了其中增删改查等行为的抽象
2.特点
- 抽象类
抽象类可以包含抽象方法
含有抽象方法的类是抽象类
//抽象类一定含有抽象方法 ;error
- abstract 修饰类,方法
- 不能实例化对象
- 实例类 extends 抽象类{ }
- 接口
- interface修饰
- 接口不能实例化对象
- 抽象方法:public abstract 常量:public static final
- 使用:class Test implements 接口1,接口2
如果既要继承类,又要实现接口,则先继承,后实现:
class MyArrayList extends 类 implements 接口1,接口2{ }
十、异常
1.类型
基础:数组,空指针,反射
MySQL
多线程网络
举2个左右例子,找熟悉领域谈,引导面试官~
2.继承结构
2~3个例子
3.异常处理机制
try catch finally:
try{
可能发生异常的代码;
}catch(关心异常的具体类,不关心异常基类处理){
}finally{
无论代码是否发生异常,必定会被执行;
}
throw:
int peek(){
if(size == 0){
throw 异常对象;
}
return element[size-1];
}
throws:
void fun() throws 异常类{
Thread.sleep(1000);
}
十一、Object类中的方法
clone()
clone方法:protected 保护权限
对象拷贝,实现Cloneable接口
class Student implements Cloneable{ //int id;int passwd; 深拷贝
@Override
protected native Object clone() throws CloneNotSupportedException{
return super.clone(); //浅拷贝
}
}
getClass()
getClass方法:获取当前类的Class对象(获得运行时类型)
toString()
People p = new People();
System.out.println(p.toString()); //默认打印类型@地址
一般需要在类中对Object toString()进行重写
equals()
“equals"和”= ="的区别
- 基本数据类型 byte,short,char,int,long,float,double,boolean
基本类型的比较应用双等号"= =",比较的是他们的值 - 复合数据类型(类)
当复合数据类型用"= =“进行比较的时候,比较的是他们在内存中的存放地址。所以,除非是同一个new出来的对象,否则用”= =“比较后结果为false。
JAVA当中所有的类都是继承于Object这个基类的,在Object中的基类中定义了一个equals的方法,这个方法的初始行为是比较对象的内存地址,但在一些类库当中这个方法被覆盖掉了,如String,Integer,Date,在这些类当中equals有其自身的实现,而不再是比较类在堆内存中的存放地址了。
对于复合数据类型之间进行equals比较,在没有重写equals方法的情况下,他们之间的比较还是基于他们在内存中的存放位置的地址值的,因为Object的equals方法也是用”= =“进行比较的,所以比较后的结果跟双等号”=="的结果相同。
finalize()
finalize()是Object中的方法,当垃圾回收器将要回收对象所占内存之前被调用,即当一个对象被虚拟机宣告死亡时会先调用finalize()方法,让该对象处理后事
此方法有很大的不确定性(不保证方法中的任务执行完)而且运行代价较高。所以用来回收资源也不会有什么好的表现。
一般不会用到此方法~
问的大多是final,finally,finalize的区别=。=
hashCode()
集合中进行对象比较,为什么既要重写equals()方法,又要重写hashCode()方法?
p1.equals(p2) -> true // 1式
p1.hashCode() = = p2.hashCode() // 2式
上面两式中,1式一定可以推出2式,而2式却不一定推出1式,因为equals不相等的两个对象的hashCode也可能相等。这又是为什么呢?
public class Object {
public native int hashCode();
public boolean equals(Object obj) {
return (this = = obj);
}
}
因为对象在不重写的情况下使用的是Object的equals方法和hashCode方法,从Object类的源码我们知道,默认的equals 判断的是两个对象的引用指向的是不是同一个对象,而hashCode是根据对象地址生成一个整数数值,而这个数值不是无限的,而是在此区间内产生:[-2147483648,+2147483647]
在java中对象可以有很多很多,通过new关键字来产生,hash码也是通过特定算法得到的,而算法无法保证所有对象的hash码在这个范围内互不相同。也就是说在上述情况下发生哈希碰撞是很常见的,因此不同对象可能有相同的HashCode的返回值。
因此在集合中进行对象比较,既要重写equals()方法,又要重写hashCode()方法。
wait()、notify()、notifyAll()
线程间通信的方法wait、notify和notifyAll是Object类具有的方法,即所有的对象实例都具有这个方法
wait():当对象调用wait方法,会导致当前持有该对象锁的线程等待。直到该对象的另一个持有锁的线程调用notify、notifyAll唤醒
notify():调用对象的notify方法,会导致持有该对象锁的所有线程中随机的某一个线程被唤醒
notifyAll();调用对象的notify方法,会导致持有该对象锁的所有线程中所有线程被唤醒
注意问题:
1、调用notify或wait必须是作用于同一个对象
2、对于notify、notifyAll、wait的调用,必须是在该对象的同步方法或同步代码块中中
3、wait方法在调用进入阻塞之前会释放锁,其他线程才能获取锁,获取锁进行通知notify操作
十二、类加载
类加载时机:
1.new
2.类名.static成员
3.反射
4.main函数所在的类优先被加载
5.先初始化父类,再初始化子类
子类的初始化顺序:
父类 静态变量 父类静态块
子类 静态变量 子类静态块
父类的实例变量 实例块 构造
子类的实例变量 实例块 构造
1.装载阶段
面试常问点:
什么是双亲委派模型?双亲委派模型的优点?破坏双亲委派模型
双亲委派模型
装载阶段产物:Class对象
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
双亲委派模型的优点
安全性,避免类的重复加载。
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应用程序也将会变得一片混乱。如果有兴趣写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但永远无法被加载运行。
破坏双亲委派模型
第一次破坏:
由于双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader则在JDK1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法唯一逻辑就是去调用自己的loadClass()。
第二次破坏:
双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷所导致的,双亲委派很好地解决了各个类加载器的基础类的同一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API,但世事往往没有绝对的完美,如果基础类又要调用回用户的代码,那该怎么办了?
这并非是不可能的事情,一个典型的例子就是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码,但启动类加载器不可能“认识”这些代码。那该怎么办?
为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
有了线程上下文加载器,JNDI服务就可以使用它去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。
第三次破坏:
双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简答的说就是机器不用重启,只要部署上就能用。
OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi幻境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当受到类加载请求时,OSGi将按照下面的顺序进行类搜索:
1)将java.*开头的类委派给父类加载器加载。
2)否则,将委派列表名单内的类委派给父类加载器加载。
3)否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
7)否则,类查找失败。
上面的查找顺序中只有开头两点仍然符合双亲委派规则,其余的类查找都是在平级的类加载器中进行的。
双亲委派模型破坏举例(JDBC,淘宝面试题)
原生的JDBC中Driver驱动本身只是一个接口,并没有具体的实现,具体的实现是由不同数据库类型去实现的。例如,MySQL的mysql-connector-.jar中的Driver类具体实现的。 原生的JDBC中的类是放在rt.jar包的,是由启动类加载器进行类加载的,在JDBC中的Driver类中需要动态去加载不同数据库类型的Driver类,而mysql-connector-.jar中的Driver类是用户自己写的代码,那启动类加载器肯定是不能进行加载的,既然是自己编写的代码,那就需要由应用程序启动类去进行类加载。于是乎,这个时候就引入线程上下文件类加载器(Thread Context ClassLoader)。有了这个东西之后,程序就可以把原本需要由启动类加载器进行加载的类,由应用程序类加载器去进行加载了。
2.链接阶段
验证:字节码文件格式,JDK版本匹配…
准备:静态变量分配内存,并赋类型默认值
解析:符号引用,转变为直接引用的过程
3.初始化阶段
静态变量赋值操作
静态块引用
class People{
static int count;
static{
count = 10;
}
}
十三、反射
People p1 = new PeopleI(“zs”);
1.拿到当前类的Class对象
-
Class<?> c = People.class;
-
People p = new People();
Class<?> c = p.getClass(); -
Class<?> c = Class.forName(“People”);
2.通过Class对象获取构造方法,生成当前类的对象
Class<?> c = People.class;
Constructor con = c.getDeclaredConstructor(String class); // 存在有参(String name)
// 如果构造私有,则获取权限
con.setAccessible(true);
Object o = con.newInstance("zs");
3.通过Class对象获取实例方法eat(),进行方法调用
方法调用:
Method method = c.getDeclaredMethod(“eat”);
method.invoke(o);