final 字面意思是 “最终的”,就是说无论用它来修饰字段,方法还是类,都表示它是最终的,不可变了。具体来说,一个 final 类不可被继承,final 方法不可被重写, final 字段不可被修改,但对于引用类型可以更改其内部的值。实践中通常将一个常量,或者确定在之后的代码中不应该被修改的字段声明为 final。
class MinStack {
// 两个栈:主栈和辅助栈
Stack<Integer> st1;
Stack<Integer> st2;
public MinStack() {
st1 = new Stack<>();
st2 = new Stack<>();
}
public void push(int val) {
if (st1.empty()) {
st1.push(val);
st2.push(val);
} else {
st1.push(val);
int min = st2.peek();
min = min < val ? min : val;
st2.push(min); // 辅助栈只记录当前的最小值
}
}
public void pop() {
st1.pop();
st2.pop();
}
public int top() {
return st1.peek();
}
public int getMin() {
return st2.peek();
}
}
辅助栈 pro
class MinStack {
Stack<Node> st;
// 用一个 Node 同时存储 val 和当前的 min 值
class Node {
int val;
int min;
Node(int val, int min) {
this.val = val;
this.min = min;
}
}
public MinStack() {
st = new Stack<Node>();
}
public void push(int val) {
if (st.empty()) {
Node tmp = new Node(val, val);
st.push(tmp);
} else {
Node pre = st.peek();
int min = pre.min < val ? pre.min : val;
Node tmp = new Node(val, min);
st.push(tmp);
}
}
public void pop() {
st.pop();
}
public int top() {
return st.peek().val;
}
public int getMin() {
return st.peek().min;
}
}
进阶:辅助常量
class MinStack {
Stack<Integer> st;
int min;
public MinStack() {
st = new Stack<>();
}
public void push(int val) {
if (st.empty()) {
min = val;
st.push(0);
} else {
int diff = val - min; // 可能会发生溢出
if (diff < 0) { // 差值小于零,意味着当前 val 为最小,需要更新 min 值
min = val;
}
st.push(diff);
}
}
public void pop() {
int tmp = st.pop();
if (tmp < 0) { // 如果弹出了最小值,就要记得更新 min 值
min = min - tmp;
}
}
public int top() {
return st.peek() < 0 ? min : st.peek() + min;
}
public int getMin() {
return min;
}
}
class MinStack {
// 两个栈:主栈和辅助栈
Stack<Integer> st1;
Stack<Integer> st2;
public MinStack() {
st1 = new Stack<>();
st2 = new Stack<>();
}
public void push(int val) {
if (st1.empty()) {
st1.push(val);
st2.push(val);
} else {
st1.push(val);
int min = st2.peek();
min = min < val ? min : val;
st2.push(min); // 辅助栈只记录当前的最小值
}
}
public void pop() {
st1.pop();
st2.pop();
}
public int top() {
return st1.peek();
}
public int getMin() {
return st2.peek();
}
}
辅助栈 pro
class MinStack {
Stack<Node> st;
// 用一个 Node 同时存储 val 和当前的 min 值
class Node {
int val;
int min;
Node(int val, int min) {
this.val = val;
this.min = min;
}
}
public MinStack() {
st = new Stack<Node>();
}
public void push(int val) {
if (st.empty()) {
Node tmp = new Node(val, val);
st.push(tmp);
} else {
Node pre = st.peek();
int min = pre.min < val ? pre.min : val;
Node tmp = new Node(val, min);
st.push(tmp);
}
}
public void pop() {
st.pop();
}
public int top() {
return st.peek().val;
}
public int getMin() {
return st.peek().min;
}
}
进阶:辅助常量
class MinStack {
Stack<Integer> st;
int min;
public MinStack() {
st = new Stack<>();
}
public void push(int val) {
if (st.empty()) {
min = val;
st.push(0);
} else {
int diff = val - min; // 可能会发生溢出
if (diff < 0) { // 差值小于零,意味着当前 val 为最小,需要更新 min 值
min = val;
}
st.push(diff);
}
}
public void pop() {
int tmp = st.pop();
if (tmp < 0) { // 如果弹出了最小值,就要记得更新 min 值
min = min - tmp;
}
}
public int top() {
return st.peek() < 0 ? min : st.peek() + min;
}
public int getMin() {
return min;
}
}
我的理解是动态代理,实际上就是在不改变原有代码的情况下对原有的方法增强。对于一些方法,他们可能需要一些统一的处理逻辑,例如打印日志,这时候我们就可以通过创建代理对象,来对原有方法进行功能上的加强。实现动态代理有两种方式,一种是通过jdk reflect包提供的proxy类实现,还有一种是通过cglib的enhancer实现。
对于jdk proxy它是面向接口的动态代理,也就是说只有一个类实现了接口,我们才能对它进行代理,本质上来说就是这个代理对象实现了被代理对象的接口,所以它只能增强接口中的方法,具体代码逻辑是通过重写invokationhandler的invoke方法,通过反射的方式对原有方法进行增强。
对于cglib的enhancer它是面向父类的动态代理,也就是说它代理一个对象就是通过继承被代理对象对原有方法增强,这就意味着它可以增强被代理对象的所有方法,并且由于反射机制的存在,可以获取到父类方法上的所有注解。
动态代理经常出现在框架中,例如mybatis通过面向接口的动态代理,对接口进行实现。在spring aop机制中,通过动态代理机制,对方法进行增强,aop中的前置通知,返回通知,异常通知等,都是通过在动态代理过程中,在相对原方法的不同位置执行对应逻辑而实现的。
stringbuilder和stringbuffer都是abstractstringbuilder的子类,但是stringbuffer中的方法都是被synchronized关键字修饰的,也就代表着stringbuffer是线程安全的,但也意味着会影响运行效率,所以在线程安全的环境下使用stringbuilder否则使用stringbuffer。以append方法举例,这两个类的append方法基本一致都是调用父类的append方法但是stringbuffer因为维护了一个toStringcache所以在对字符串修改时会先清空缓存。还有一点需要补充,就是虽然stringbuffer的方法被synchronized修饰,但是锁住的只是当前的stringbuffer对象,如果拼接的是string对象那无可厚非,但是如果拼接的是另外一个stringbuffer或stringbuilder,我们无法保证它们是线程安全的。
final一般修饰与类或者变量上代表这个类不可被继承,或者这个变量不可被修改或这个引用不能指向其他对象。
对于不希望被修改的值或继承的类,我们都可以通过final修饰。并且因为final变量的初衷就是告诉编译器,我这个变量是不可变的,编译器可以随意优化(重排序),并且在java1.5后对构造器内final的重排序进行了约束,不会发生逸出现象。还有一点通过static和final搭配使用,在使用该变量时不会导致类加载。
1.String 为什么要设计为不可变类?
这个问题可以从两个角度来分析
从安全性来看,String被final修饰就意味着该类不可被继承,并且底层的char数组被private修饰且没有对外访问的接口,这样在多线程的环境下,我们就不需要担心String类型的线程安全问题,因为每次对String的修改都指的是创建一个新对象,或是由字符串常量池中返回一个对象。
从性能的角度分析,首先java为了使字符串可以复用,在jvm中有专门的字符串常量池存放String对象,如果String提供给外界可以修改字符串的接口,那么这个字符串常量池也失去了它的意义,并且因为String类型经常作为HashMap中的key所以String中就专门缓存了用private修饰的hash值,这个hash值在第一次调用String重写的hashcode方法后就会缓存到对象中,提高了效率,而且由于String是final修饰的,我们也就不用担心由于它的哈希值改变在通过key查询value时,查询不到值。
2.String a = new String(“aa”) + “bb” “这句话创建了多少个对象
如果字面量“aa”和“bb”在之前都没有出现过,那么首先会在字符串常量池中创建两个String对象,之后会创建一个StringBuilder对象调用append方法拼接“aa”和“bb”,最后调用toString在堆内存中创建了一个String对象存放“aabb”,假如这两个垃圾对象StringBuilder和new String(“aa”)都被gc那么,在最后的状态在堆内存中存在3个String对象,但是在这个过程中创建了5个对象
3.String 对象最多可以存放多少个字符(长度)?
理论上是Integer.MAX_VALUE大小个字符,也就是21亿多,因为String底层使用char数组存放字符串,而通过String的length()方法可以发现,char[]的长度是int类型,所以理论上的最大值是21亿,但是由于很多虚拟机会在数组中加入一些元数据,所以精确来讲可以存放Integer.MAX_VALUE – 8个字符,这一点可以在ArrayList源码中定义的数组最大长度可以发现。
4.字符串常量池是放在堆中吗?
在java6及以前常量池放在永久代中,java7开始字符串常量池从永久代移动到堆内存中,这样设计可能字符串常量池就可以存放更多的数据,并且java8开始永久代就被废除,方法区的实现变成了元数据空间。
5.String中 “+” 和 StringBuffer 中的 append 会有性能上的差别吗?
会有。如果使用+就可能会创建新的String对象,而使用StringBuffer的append方法只是对原有的char[]数组修改,具体的实现逻辑就是判断char[]数组是否需要扩容,之后调用System.arraycopy方法将String中的char[]拷贝到StringBuffer中的char[]中,不需要创建一个新的对象。
多态可以理解为 “事物运行时的不同状态”,在 Java 中具体指,通过动态绑定,在运行时根据对象的实际类型来调用对应的方法。多态可以通过继承或者接口来实现。拿继承来说,子类必须重写父类的方法,通过向上转型,使用父类类型来调用子类对象的方法,结果是在运行时执行子类中重写的方法。
实践中我的项目的支付模块就用到了多态。具体来说,有一个基类 “支付方式 Payment”,它有一个 pay 方法,之后分别创建 “微信支付 WeChatPay” 和 “支付宝支付 AliPay” 两个类,并且都继承自 Payment 并重写 pay 方法。那么在之后的支付代码中,无论用户选择的哪种支付方式,都可以统一用 Payment 来进行具体的 pay 操作。原因是 Java 的动态绑定会确保这个方法在真正执行时,去调用子类中微信或者支付宝的 pay 方法。另外,在修改具体的支付逻辑时,也只需单独去微信或者支付宝的类里面去修改,而不用动主逻辑里面的代码,体现出多态的灵活和可扩展性。
除了以上说的运行时多态,我还听说过一个有争议的编译时多态,它具体指 Java 中的方法重载,在编译期就根据已知的参数列表,决定了需要调用的方法。不过一般说多态都默认说的是运行时多态。
final 字面意思是 “最终的”,就是说无论用它来修饰字段,方法还是类,都表示它是最终的,不可变了。具体来说,一个 final 类不可被继承,final 方法不可被重写, final 字段不可被修改,但对于引用类型可以更改其内部的值。实践中通常将一个常量,或者确定在之后的代码中不应该被修改的字段声明为 final。
重载是在一个类中定义多个方法名相同,但是参数类型,参数个数或参数顺序不同的做法。
重写就是子类要重写一个父类中的方法,所以方法名和参数要一定相同。两个本身没什么必然关系吧
equals( ) 和hashCode( ) 是 Object 类的方法,equals( )默认是比较引用类型的地址值,在想要比较数值相同时候,重写。hashcode,方法能生成一个hash值,然后根据值是否相同来做一些事情。在比如集合那块,hashset(底层是hashmap),要往table放的话,首先计算哈希值,转成索引值,没有直接放,有的话就要比较equals了,这时候equals也要重写,不重写可能也加进去了。就和我们的想法违背了。用快捷键重写。。。
判断基数据类型是判断值是不是相等,判断引用数据类型是判断地址值
equal是判断在堆中是不是一样的地址值,一般会重写。
equals表示判断两个的地址是不是一样的,因为string重写了equals方法,所以有些人认为是判断值是否相等
getclass获取当前运行类的对象
tostring默认返回的是地址值,可以自己重写
hashCode的方法用来获的哈希值,在集合set中判断元素要不要加进去,就要用到,先判断hash值,然后再看equals是不是一样。
抽象是被 abstract 修饰的类或者方法,接口是被interface 修饰的类。在使用上,子类extends抽象类,implements接口,抽象类是单继承的,但是接口不是,可以被多个实现,子类可以重写抽象类的方法,满足自己的需求,接口也一样,但是接口可以规范行为,比如在公司几个人要写不同的类,可以让他实现我们定义的接口,这样子方便统一管理
1,异常是程序执行中出现的一些意外,就需要我们来解决。
根父类是throwable,包括 Error 和 Exception,一般我们要解决的异常是编译是异常,运行时异常应该避免,error是jvm死机了,严重错误。例子:空指针异常(NullPointerException),角标越界(ArrayIndexOutOfBoundsException),类型不匹配异常(InputMismatchException)等
3.throw是我们自己抛出来异常,需要去解决,或者往上抛,throws则就是不解决往上抛的那个,抛给方法的调用者,当然一般建议别抛到main函数,在main之前try catch。 直接抛出去可能要抛出去多次,每次都try catch影响性能吧,可以交给一个最上面的调用者一块儿解决。
4,出现异常的时候影响,不出就不影响
5,执行,程序先把catch的return暂时存放,然后finally执行后才返回,但是当程序进入 try 块之前就出现异常时,就结束,不会执行 finally 块中的代码。
1,为了性能,在常量池有一个值,如果下次还要用就不用在new了。直接从常量池里拿就行了。然后为了安全,多个线程共享一个string,不用同步处理。
2,一共5个?创建了4个string对象,1个stringbuilder,先调用stringbuilder构造器,第一个。然后常量池一个aa,一个bb,然后在堆空间2个new出来的值分别指向常量池的aa,bb.最后StringBuilder完成字符串变量拼接完成后返回的 toString()方法中还创建一个新的对象aabb。
3.字符串的最大长度2^31-1。(网上查的:编译期的限制:字符串的UTF8编码值的字节数不能超过65535,字符串的长度不能超过65534;
运行时限制:字符串的长度不能超过2^31-1,占用的内存数不能超过虚拟机能够提供的最大值。)
4. 1.8之后放在常量池(元数据),之前在堆吧(永久代)5
5.有的,string每次加都要创建新对象,但是stringbuilder可以在后面跟着加,所以后面的性能会更好。
类加上final证明不能在被继承,方法加上不能重写,变量加上只能赋值一次,比如 final int x = 1,x++就是错的,但是x+1是可以成功的,就是说对象不能再变,但是里面的内容的值是可以变的。一般配上static用(我是菜鸡,大佬们看到哪儿错了可以踢我一下,谢谢)
Java 实现多态有三个必要条件:继承、重写和向上转型。也是编译类型看的是左边,运行类型其实是右边,在接口的实现类也可以用,也可以当参数传给形参,在使用多态时,子类方法完成了对父类方法的重写后,父类去调用子类的方法而不去调用父类的方法。但是编译还是父类,实际执行是子类的方法,好处是可以方便使用,不用每次都去创建。还能可以提高程序的扩展性。 (前父后子),父类型的引用指向子类型的对象 多态除还可以解耦。
两个都是可变字符串,StringBuilder是线程不安全的,但是速度快,stringbuffer是安全的,同样速度相对慢一点,都不能被继承,被final修饰,都实现了serialzable接口,也能串行化在网络中传输,内容都存在父类的abstractstringbuilder的char【】vale中。
默认情况下,equals()比较的是两个对象的内存地址是否相同,而hashCode()是将对象的内容或值,经过哈希函数运算之后,返回一个int类型的结果。这两个方法都用于两个对象之间的比较。对于HashMap HashSet等集合来说,比较两个对象的大致流程是,先计算出两个对象的hashCode,如果hashCode不相等,说明这两个对象不相同;如果hashCode相等,就用equals方法再进行比较,判断这两个对象是否相同。
一般来说,对于自定义的类,都需要对equals和hashCode这两个方法进行重写,保证集合在处理这些类的对象时,能够正确地存储和查找对象。
重写这两个方法,使用Lombok的注解@Data,是比较简单的方式。
对于equals方法,传进来一个Object类型的对象,如果这个对象为空,或者跟当前对象不是属于同一个类,直接返回false。再把传进来的对象,本来Object类型,强转为当前对象的类型,比较这两个对象的各个属性是否相等。
对于hashCode方法,直接调用Objects类的hash方法,把这个类的所有属性传进去。
hashcode:通过key值生成一个hash值,主要用于哈希表的映射。
equals:比较两个对象是否相等,这里比较的是地址是否相同。
我们在实现业务需求时可以重写equals和hashcode方法。
wait、notify、notifyAll主要用于多线程并发场景,等待、唤醒、唤醒全部的功能。
getClass:获取对象的类,返回一个类对象。
toString:默认返回string类型对象的的类名和引用地址,我们可以对它重写以满足自己的业务需求。
Java中对源代码进行编译,生成class文件,可以运行在不同的操作系统上,无需对源代码做任何修改,就是平台无关。
我们平常写的java源代码编译后成为class类型的字节码,该字节码可以运行在jvm虚拟机上面,而我们虚拟机就给我们实现了一些关系平台相关的操作,程序员是看不到的,所以只要在不同的操作系统安装jvm虚拟机,就可以实现平台无关性,所谓的平台无关性也是站在程序员的角度平台无关的。
equals是object类中的一个方法,用于比较两个对象的引用地址,也就是对象所在堆内存的地址,在平常使用中我们可以重写equals方法,以满足我们的业务需求。
是一个双目运算符,比较两个两个基本数据类型的值是否相等,比较引用数据类型时比较两个对象的地址是否相同。
equals是object类中的一个方法,用于比较两个对象的引用地址,也就是对象所在堆内存的地址,在平常使用中我们可以重写equals方法,以满足我们的业务需求。
是一个双目运算符,比较两个两个基本数据类型的值是否相等,比较引用数据类型时比较两个对象的地址是否相同。
1、在Java中,由于会大量的使用String常量,如果每一次声明一个String都创建一个String对象,那将会造成极大的空间资源的浪费。Java提出了String池的概念,在堆中开辟一块存储空间字符串池,当初始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。另外就是安全性,多个线程共享一个string时是线程安全的,因为不会有修改string造成的线程不安全问题。
2、4个,首先常量池找aa,没有就创建后放入,此时常量池一个对象,new的时候产生一个对象放放在堆内存中,引用地址指向常量池中aa对象地址,bb也在常量池中有一个对象,然后就是常量池中有一个aabb的对象。(总之new一个对象就会在堆内存中单独生成一个对象)
3、string中的字符是用char[int]来存储的,int的长度为2^31-1,这是理论长度,由于常量池限制,在常量池中string最大长度为2^16-2。堆栈中是我们的虚拟机堆内存限制
4、不是的
5、有的,前者的内存和CPU开销会大于后者,毕竟安全高和开销小不可兼得嘛。
1、异常是指我们的程序在执行的过程中出现的一些错误,如果程序在执行过程中发生错误,我们希望程序能够根据我们的异常寻找一个出口,告知到我们的客户或者程序员,能够使程序正常的退出。
2、异常分为error和exception异常,他们都是Throwable的子类。error异常主要是虚拟机异常,比如系统崩溃、虚拟机错误、内存不足、堆栈溢出等问题,该异常无法通过修改程序代码来解决。exception是程序的问题,又被分为运行时异常和编译时异常。
3、throw主要是在方法体内部抛出一个异常,由方法体内部进行处理,是一个抛出的动作,抛出的是一个异常实例。throws是在方法声明部分的后面使用,向上抛出异常,将抛出的异常交给方法调用者来解决。对于异常,我们不一定是只会在这一层会产生这个异常,如果我们在每个产生该异常的地方都使用trycatch抛出,那么相同的异常我们可能需要在同一个程序中抛出多次,影响程序的可读性,并且会带来多次处理异常的开销。
4、try…catch本身不会影响系统性能,但是我们出现异常时系统处理我们的异常会带来系统开销。
5、会的,程序会将我们的return值存储起来,然后执行finally中的内容,对于基本数据类型,如果finally中修改了该数据不会对返回造成影响,如果是引用数据类型,会返回finally中修改后的该引用的对象。
equals()是一个Object类的常用方法,用于引用类型的比较,默认实现是比较两个对象的内存地址,但是很多类都重写了equals方法,使得比较的是对象的属性和对应的值,例如String Integer等类,比较是否相等时,都应该用equals();
而多用在基本数据类型的比较,比如int char等,可以直接比较它们的值是否相等。
Object()类的常用方法有getClass()、toString()、hashCode()、equals()、notify()、notifyAll()这几个。
getClass()是用来获取一个对象的类,返回这个对象的类对象,getClass方法只能看这个对象直属的那个类,而如果要看这个对象的父类的话,就需要用到getSuperClass()方法;
toString()默认返回String类型的类名加地址,对于一个自定义的类,如果想要用toString来表示它的属性和对应值的话,可以重写这个方法;
hashCode()根据对象的内容或值,经过哈希函数运算之后,返回一个int类型的结果,一般来说,相同的两个对象,hashCode一定相等,但是hashCode相等的两个对象,不一定相同,所以如果要判断两个对象的各个属性值是否完全一样,还需要用到equals()方法,hashCode相比于equals,底层不需要其它复杂的操作,速度较快;
equals()用来判断两个对象是否相同。对于自定义的对象,一般要同时重写equals()和hashCode()方法,来保证hashMap、HashSet等集合,在判断冲突的时候,不至于出错;
notify()和notifyAll()用于唤醒等待某个资源的线程。notify是随机唤醒一个线程,而notifyAll是唤醒所有线程。这两个方法都只能在同步方法或同步代码块中使用。一般建议使用notifyAll,因为使用notify可能导致某些线程永远不会被唤醒,而导致无意义的等待。例如:两个资源A B,线程1占用了A,线程2占用了B,此时有个线程3申请使用A,线程4申请使用B,因为线程1 2还没释放A B的锁,线程3 4只能进入等待队列进行等待。在某个时刻,线程1释放了A,如果使用notify,那么可能恰好唤醒了线程4,而线程4不是想要资源A,所以会继续等待,而想要获取资源A的线程3却没有被唤醒,导致一直在做无意义的等待。
时间O(3(N+M)),空间O(2(N+M))。递归练习,有些耗空间。
1、这个主要是出于线程安全和性能方面的考虑。线程安全体现在,由于 String 是不可变的,多个线程共享一个 String 时不用担心它的同步问题;性能体现在缓存哈希值和设计常量池上。String 在被创建时就缓存了自己的哈希值,使用时直接拿出来就行,不用重新计算,这使得 String 适合用来作为 Map 的 Key,可以快速获得 Key 的哈希值,提高查找和比较的效率;除此之外,基于 String 的不可变,Java 使用常量池来尽可能的共享相同的字符串,来节约 String 的存储空间。具体的,当我们使用字面值创建 String 时,会先去查它是否已经存在于常量池中,如果是则直接返回这个已存在字符串的引用,而不会创建新的对象。
2、共创建了 4 个 String 对象,第一个是常量池中的对象 “aa”,如果常量池中有 “aa” 就直接返回,没有就创建并添加进常量池中。第二个是 new 出来的以 “aa” 为初始值创建的 String 对象,第三个 “bb” 同 “aa”,第四个是通过 “+” 拼接前两个对象,创建出的新 String 对象。
3、String 在源码中使用 char[] 来维护字符序列的,而 char[] 的长度是 int 类型,所以理论上 String 的长度最大为 2^{31}-1 ,占用空间大约为 4 GB,不过根据实际 JVM 的堆内存限制,我自己电脑上测试是 2^{31}-3。
4、不是,Java 8 以前被放在永久代中,Java 8 及以后被放在方法区的元数据 metadata 中。
5、String 的 “+” 效率低于 StringBuffer 的 append( )。原因在于 String 是不可变类,任何对 String 的操作都会创建新的 String 对象。而 “+” 的执行过程实际上是先创建了一个 StringBuffer,然后调用 append( ),最后在 toString( )。效率上肯定是不如 StringBuffer 直接 append( ) 高的。
将之前L、R的思想互换一下。在mid*mid > x时,R反而可能是我们需要求的。
人都会发错,更何况程序,如果程序出错,我们希望可以把之前程序完成的工作保存下来然后正常退出,而不是丢失已经完成的结果。这就是异常需要完成的事情。异常分为Error和Exception。Error就是错误,这种错误会导致程序无法运行,比如堆溢出、栈溢出等。想IO异常、套接字异常等这些都是受检查型异常,这些异常在我们编写程序时就可以知道可能会发生的异常,当你打开一个文件时,就知道这个文件可能不存在,所以需要throw来抛出一个文件不存在异常,必须使用catch或者throws处理这个受检查型异常。try包住可能会抛出文件不存在的异常的代码块,然后catch捕获文件不存在异常,在catch的代码块写入处理逻辑。而数组越界异常、空指针异常这些都是程序运行时,才可能会发生的异常,大都不可能知道在哪些地方可以具体捕获,所以无法使用try包住,也无法 catch。这些非受检异常抛出后可以不用捕获,当然实际开发中只有没事做才自己写代码抛出这些非受检查型异常。当你知道自己的一个方法可能会发生某一种异常,但不想处理,可以使用throw声明这种异常,让调用这个方法的人自己处理。try catch只有异常发生时才会影响异常,因为发生了异常,肯定要构造异常对象,异常对象的构造,比如异常发生在哪一行,可能需要收集异常栈,无疑是一个耗时的操作。如果catch中return,finally也会执行。catch也抛出异常,fianlly一样执行。所以finally中一般做一些资源清理的工作,最好不在在finally在抛出异常或者执行return,因为finally抛出的异常会覆盖catch中的异常,return的返回值也会覆盖catch中return的值。
同一套代码,在不同操作系统上运行,不需要对源代码做任何修改,即可顺利运行,这就叫做平台无关性。
java之所以可以实现平台无关性,依靠的是JVM和字节码(Class文件)。java编译器首先把.java文件编译打包成字节码,然后启动JVM,由JVM在不同平台上运行这些字节码,所以java代码的运行是不用直接跟操作系统交互的,而是通过JVM作为中间层,来屏蔽平台的差异,所以java程序员只需要专注于java程序的开发逻辑即可,而不需要过多的关注平台的细节。当然,这并不意味着java程序完全与平台无关,还是要注意一些可能导致问题的因素,例如:文件路径、编码格式、系统属性等。
抽象主要是为了代码复用。比如一些类拥有一些通用的功能,为了不在每一类中重新写一遍这个方法的代码,就可以定义一个抽象类,这样一来,只需让每一个类继承抽象类就可以了,如果后期需要修改方法,只需要修改抽象类中的方法就行,如果子类想要自己实现不一样的行为,只需子类重写抽象类的方法。这样一来,普通类就可以完成这个工作,为什么需要抽象类,抽象类还起了一个限制作用,比如要求每一个子类必须自己独特实现的一个方法,也是抽象类定义的抽象方法。缺点也很明显,因为Java没有多继承,导致一个类只能继承一个父类。在表示是什么的关系时,一般使用抽象类。
接口更多的是为了解耦。比如我需要制定一套方法的规范,就可以将这套方法规范抽象为一个接口。每一个继承这个接口的类都必须实现接口中所有的方法。在表示有什么的关系时,使用接口。
重载和重写都是多态的体现。Java中每一个类都有一个方法表,如果一个类中有同名的方法,只要参数类型或者个数、排列顺序不同、返回类型不同,Java就会认为它是一个不同的方法,放入方法表中。当调用同名方法时,就会根据参数列表去方法表中找到对应的方法执行,具体调用哪一个方法编译时就已经确定。所以重载是编译时多态的体现。而重写是运行时多态的体现,如果调用一个类的方法时,该方法在类的方法表找不到,Java就会去父类的方法表中找,找不到再去Object类中的方法表找,具体调用哪一个方法是运行时确定的。如果调用子类重写的方法时,方法的符合引用解析后指向子类中方法表的索引值(该索引值和父类方法重写方法的索引值一样),所以重写是运行时多态的体现。
重写适用于子类想要有自己独特的行为。重载适用于向针对不同参数列表分别实现不同功能的场景。
打卡
打卡
1.异常是指程序中出现的错误,因为java对异常进行了详细的分类所以通过异常可以使程序员确定错误发生的位置和原因,程序员就可以有针对性对这些异常进行处理
2.异常可以分为error和exception
error通常是指虚拟机发生的严重错误,我们无法通过修改程序对这些错误进行解决,常见的error有stcakoverflow,oom等,我们通常需要修改虚拟机的参数来解决
exception是可以通过对程序进行调整而解决的,exception又可以分为受检异常和非受检严查,受检异常也可以叫做编译时异常,例如filenotfound exception 这些异常是要求程序在运行前就必须进行处理或向上抛出的,也就是说针对受检异常,我们必须要有对应的解决方案
非受检异常是也可以叫做运行时异常,是指在程序运行时由于逻辑错误发生的异常,例如npe,程序编写时不需要处理,默认是将这些异常向上抛出,直到抛到虚拟机后打印出异常。
3.throw是在一个方法体内制造出一个异常,throws是跟在方法参数列表后,表示方法中发生的这些异常统一向上(调用者)抛出。
我的理解是,针对异常,我们可以有一个统一的解决方案,所以我们可以将异常都向上抛出之后统一处理,例如在我的springboot项目中,通过ControllerAdvice和ExcpetionHandler两个注解搭配使用,根据aop机制去处理所有在程序上游抛出的异常,交给异常处理方法去统一的处理
4.不会吧?
5.答案是会,无论如何finally块必须执行,举个例子,假如有一个变量i = 0,在catch块进行了i++紧接着return i
但是后面finally块又执行了i++,那么假如程序进行了catch块 i = 1 之后想要直接return i但是发现finally还未执行,此时就会保存i = 1这个值等finally执行完再return,所以虽然在finally中 i++ 等于2了但是仍然会返回保留的值i = 1
1、异常是程序在执行过程中可能发生的一些错误,它会暂时终止程序,并转到异常处理部分尝试恢复。异常的出现可以使我们把程序中可能出现错误的代码从正常代码中分离出来,单独进行处理。
2、类似于 Object 类,Throwable 是所有异常的父类。下一层分为 Error 类和 Exception 类。
Error 类表示严重的错误,一般由 JVM 和底层系统引发,并且不可恢复,例如内存溢出,class 没有主方法等。Exception 类是可以被程序捕获和处理的异常情况。
Exception 再往下,按照异常的性质又分为编译异常 CheckedException 和运行时异常 RuntimeException。其中检查异常在编译阶段就能被检测出,例如 IO 异常 IOException,文件找不到异常 FileNotFoundException等。运行时异常只有在程序运行时才能被检测出,例如空指针异常 NullPointerException,数组下标越界 ArrayIndexOutOfBoundsException 等。
3、throw 用在方法体内,表示某个地方需要抛出一个异常,是一个具体的动作。而 throws 用在方法声明后面,表示这个方法可能会抛出哪些异常,是一个声明。
这个需要分情况对待。首先,对于能在方法内处理的异常,可以直接用 try catch,但对于在当前方法内无法处理的异常,我们只能选择抛出,将它交给方法调用者去处理。其次,有时多个方法可能会抛出相同类型的异常,如果每个方法都用 try catch 会非常冗余,这时可以在这些方法中只 throw,在调用这些方法的地方整体 try catch,统一进行处理。
4、几乎不会,也就是程序没有异常时,try catch 加和不加性能几乎一样,只有在程序有异常需要捕获时才会影响性能。
原因在于,处理异常的 catch 语句在 class 文件中是用异常表实现的。异常表有四个字段,分别是 From, To, Target 和 Type。From 和 To 分别对应 try 块对应的行号,如果其中有异常抛出,会跳转到 Target 对应的行号,也就是 catch 语句对应的位置,而这个异常的类型记录在 Type 中。如果程序在 try 中没有异常抛出,最终会通过一条 goto 指令跳转到 try catch 块后的语句继续执行,这一条 goto 指令性能的消耗可以忽略不计。如果有异常,才会去查异常表,再跳转到对应的 catch 块位置去执行,这个过程可能会影响性能。
5、会的,无论 catch 块中有异常还是有返回语句,最终一定会在方法真正结束之前执行 finally。但是需要考虑一种特殊情况,就是如果 try 块中抛出了异常但没有被 catch 捕获,且此时 finally 中也抛出了异常,那么 finally 中的异常会覆盖掉 try 中的异常,成为这个方法最终抛出的异常。实践中需要注意一下这个异常覆盖问题。
索引可以类比成书的目录,通过直接翻书的形式,来找某个章节的话,可能会很慢,但是如果看了目录再翻,就会快很多。索引在数据库中的应用也是类似这样。
对于Mysql的索引,它的数据结构采用B+树,使用B+树来存储索引,B+树有以下几个特点,第一,一个结点的子结点可以大于两个,这可以减少树的层数,在磁盘的数据存储里面,层数减少意味着磁盘的寻址次数也会减少,使得查询效率得以提高;第二,数据都存在叶子结点,这个可以使得一个数据页存放更多的结点,减少查询的时候,从磁盘读写到内存的次数;而且叶子节点之间,是用指针相连,形成一个双向链表,这非常适合于数据库中常用的范围查询,知道头或尾,直接就能一整条查出来。
不同的存储引擎,内部的索引结构会有所不同。例如,常用的InnoDB,主键索引采用的是聚簇索引,而非主键索引采用的是非聚簇索引。所谓聚簇索引,就是行记录和索引都存储在一起;使用InnoDB的表,必须要有一个聚簇索引,如果定义了主键,那么主键索引就是聚簇索引,如果很不幸,这个表没有定义主键,首先选一个唯一的非空列作为聚簇索引;如果连唯一的非空列都没有,那么会选择一个隐藏字段row_id作为表的主键,并且用作这个表的聚簇索引;对于InnoDB的非主键索引,它的叶子结点存的是主键,非叶子结点存的是索引字段的值,所以在条件允许的情况下,我们不应该选择长度太大的列作为索引,这样会使得索引占用的空间太大。
MyIsam采用的都是非聚簇索引,所以它的行记录、主键索引、非主键索引,是分开三个东西来存储的。
hashcode()、equals()方法是Object类中的方法,Object类是所有类的父类,所以每个类都有这两个方法。
在Object中,equals()方法是通过比较两个对象的在堆中的地址(引用地址)来比较两个对象是否相同,hashcode()方法是用来计算对象的hash值,但是在原生的Object类中的hashcode是根据引用地址来映射的。
在实际应用的我们经常会对equals()和hashcode()方法进行重写,以达到我们判断两个对象的内容是否一致的效果。以我们的hashmap为例子,当我们在存入一个Object对象时,我们希望我们对象的内容一样时,我们就认定该对象是相同的,不能进行写入。此时如果我们使用相同的内容去new了像个对象,如果不进行重写的话,我们发现我们对比的就是两个对象的地址,我们new了两次那么两次的地址肯定是不一样的,会将两个对象放在不同的位置,达不到我们想要的结果。hashCode()用来定位要存放的位置,equal()用来判断是否相等,所以我们需要重写hashcode和equals,重写hashcode使得我们的hash值使用我们对象内容来映射,先找到对应的哈希表上是否有元素,如果有,然后重写我们的equals通过与该位置的对象的地址以及key、value值来进行比较两个对象是否相同,有任意的不同之处都证明我们不是相同的对象,可以插入,否则认为是相同的对象。
重载:是一个类中定义多个方法名相同,参数类型、参数个数或参数顺序不同的方法。
重写:是指在子类中重写一个父类中的方法,要求方法名和参数完全相同,权限修饰符范围要大于等于父类中的权限修饰符。
重载是编译期多态,在该程序进行编译时就已经将一些列重载方法给静态编译成了字节码文件;重写时运行时多态,子类重写父类的方法需要在创建子类对象并调用该重写的方法时才进行动态编译。
Java中抽象是值被abstract修饰的类或方法,如果一个类中有一个抽象方法,那么该类必须用abstract修饰,但是抽象类中可以不存在抽象方法,抽象类不能够进行实例化。普通类可以继承一个抽象类,那么这个类就是该抽象类的直接子类,子类可以调用该抽象类的非抽象方法,也可以重写该抽象类的所有方法。需要注意的是,一个普通类只能继承一个父类。
Java中的接口是用implement修饰,接口中只定义接口可以实现的方法而不给出具体的实现,继承了该接口的普通类需要实现该接口的所有的方法,接口可以说以被定义好的一种规范,所以实现了接口的普通类都需要遵守它。一个普通类可以实现多个接口,这个特点也弥补了单继承的缺点。
辅助栈
辅助栈 pro
进阶:辅助常量
equals()和hashcode()都是Object类的方法,在没有重写的情况下,equals比较的是两个对象引用是否指向同一个对象,hashcode返回的是当前对象的地址经过hash函数映射得到的哈希值
一般我们想用HashMap存储对象时需要重写这两个方法,来保证集合中不出现重复Key,这里重复Key的定义是对象的属性不能完全相等,所以我们需要重写equals,来逐一比较两个对象的属性值,但是一定也要重写hashcode,因为hashmap是先通过调用hashcode得到一个哈希值,将该哈希值按数组长度取模得到一个哈希桶的位置,再通过equals方法依次比较哈希桶上元素的key,所以同时重写这两个方法,得以保证集合中不会出现重复key,如果只重写hashcode的情况下就会造成一个哈希桶上出现相同key的元素,如果只重写equals的情况下就会造成相同元素的key出现在不同哈希桶的位置上
对于hashcode方法最简单的重写方式就是依次调用对象中每一个属性的hashcode并相加。
重载和重写都是面向对象中多态的表现
重载是编译时多态,通过使用相同的方法名,不同的参数列表和返回类型,而使得调用同名方法后表现出的状态是不一样的,这个状态是在编译器就已经决定的
重写是运行时多态,子类通过重写父类的方法,也就是方法名参数列表相同,返回类型和抛出的异常是父类的类型或子类型,访问修饰符大于父类。这样在一个父类引用指向不同子类型时,由于不同子类重写的方式不同,调用一个方法表现出的状态也不一样,这个状态是在运行时才决定的
抽象类是自下而上的,也就是将一系列具有相同属性和方法的类抽离出来的类,抽象类是不可以被实例化的,也就意味着它只是一个抽象的概念,普通类通过继承抽象类来具体化这个抽象类,使用抽象类可以让代码更加健壮可拓展性更强
而接口是自上而下的,通过定义一系列的行为也可以说一系列的规范,实现接口的类必须去遵守这些规范/行为。
= =是一个操作运算符,运算数是基本数据类型比较的是值否相等,运算数是引用数据类型比较的就是两个引用的地址是否相等。
equals()是Object类中的方法,在没有重写的情况下,equals底层就是通过 = = 来实现的,用来比较两个引用地址是否相等,也就是两个引用是否指向堆内存中同一个对象。
我们通常重写equals方法,用来比较两个对象的属性是否相等,例如各种包装类,String类。
平台无关指的是对于不同的操作系统,都可以用一种统一的格式执行代码,也就是一次编译,到处运行
我们编写的.java源程序只需要编译成.class字节码文件,之后让JVM去执行字节码文件,JVM为我们屏蔽了操作系统的差异性,所以只需要针对不同的操作系统实现对应的JVM就可以执行我们的java代码,在java开发者看来,java代码只是在一台虚拟机器上运行的,感受不到操作系统的存在。而且由于JVM只执行字节码文件,所以像kotlin这样的语言,只要可以编译成字节码文件,也可以在JVM上运行,这是JVM的语言无关性
因为散列集合插入对象时会进行判断,先调用hashcode,如果相同,再调用equals,如果都相同则只插入一个。
如果只重写了 equals 方法,那么默认情况下,散列集合(HashMap、LinkedHashMap、HashSet、 LinkedHashSet )进行去重操作时,会先判断两个对象的 hashCode 是否相同,此时因为没有重写 hashCode 方法,所以直接执行 Object 中的 hashCode 方法,而 Object 中的 hashCode 方法对比的是两个对象的引用地址,显然是不同的。所以结果是 false,那么导致 equals 方法就不执行了,直接返回的结果就是 false,所以两个对象不是相等的,于是就在散列集合中插入了两个相同的对象。(显然是错误的)
但是,如果在重写 equals 方法时,也重写了 hashCode 方法,那么在执行判断时会去执行重写的 hashCode 方法,此时对比的是两个对象的所有属性的 hashCode 是否相同,于是调用 hashCode 返回的结果就是 true,再去调用 equals 方法,发现两个对象确实是相等的,于是就返回 true 了,因此散列集合就不会存储两个一模一样的数据了,于是整个程序的执行就正常了。
总结
hashCode 和 equals 两个方法是用来协同判断两个对象是否相等的,采用这种方式的原因是可以提高程序插入和查询的速度,如果在重写 equals 时,不重写 hashCode,就会导致在某些场景下,比如将两个相等的自定义对象存储在散列集合时,就会出现程序执行的异常,为了保证程序的正常执行,所以我们就需要在重写 equals 时,也一并重写 hashCode 方法才行。
辅助栈
辅助栈 pro
进阶:辅助常量
重载是指在同一个类中定义多个方法,它们具有相同的函数名,但参数的类型,个数和顺序可能不同。它提供了一种灵活的方式来实现相似的功能。
重写是指在子类中重新定义并覆盖父类中的方法。它使子类可以根据具体的类型调用相应的方法实现。
在 JVM 中,方法重载对应 “静态分配” 的过程,也就是 JVM 在编译期就根据参数的静态类型,决定了会使用函数的哪个重载版本。而方法重写对应 “动态分配” 的过程,具体的,重写方法的调用是由字节码中的 invokevirtual 指令实现的,而 invokevirtual 指令会在运行期间,根据方法接受者的实际类型来选择方法的执行版本。
equals()方法是用来比较两个对象是否相等的。hashcode()是用来计算哈希码用来确定对象在哈希表中的位置的。比如你需要根据对象的内容来判断两个对象是否相等时,你就必须重写Object类的equals()方法,因为equals()方法Object类默认是根据对象的地址值是否相同判断两个对象是否相等。这时你就必须还要重写hashcode()方法,根据对象的内容计算哈希码,而不是根据对象的地址值计算哈希码。因为必须满足一个相等的对象的hashcode一定是相同的,但不相等的对象的hashcode可以相同,此时会产生哈希冲突。否则HashMap就不能正常使用。假设HashMap类的key重写了equals()方法,根据内容来比较两个对象是否相等,而没有重写hashcode()方法,默认使用Object类的hashcode()根据对象地址值计算哈希码。此时可能会出现一种情况:比如你put一个(key, value1)的键值对,后面又put一个(key, value2)键值对,然后你执行一个get(key)操作,取出来的值可能是value1而不是value2,为什么呢,因为这个key是第一个旧的key对象,你的key的hashcode是根据地址值计算的,而不是根据对象内容,两个key的内容虽然都是key,但地址值不一样,于是hashcode就不一样,所以(key, value1)和(key,value2)就位于不同的哈希槽上,于是HashMap中就出现两个内容相同的key。如果需要取出(key, value2)怎么办呢,你需要执行gey(key)(注意: key是第二个新的key对象,这样才能根据它的地址值计算出对应的哈希槽)。