0
点赞
收藏
分享

微信扫一扫

不一样的Java基础细节

Brose 2022-02-17 阅读 158

方法重载、重写和私有

  • 静态方法可以被继承,但是不可以被重写,与类绑定。私有方法可以被继承,但因为权限问题,无法被访问,所以不能被重写。(但可以扩大他子类的访问权限,进行“重写”,但实际上已经不算重写。)
    本质原因:非虚方法(invokestatic或invokespecial)在解析阶段就会将常量池中的符号引用转换为直接引用,虚方法在运行时期,通过动态链接到真实的类和方法,替换符号引用。

  • 重写本质:
    1、找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C。

    2、如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java. lang. IllegalAccessError异常。

    3、否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。

    4、如果始终没有找到合适的方法,则抛出java. lang . AbstractMethodError异常。

  • 虚方法表:

    在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,JVM采用在类的方法区建立-一个虚方法表(virtual method table) (非虛方法不会出现在表中)来实现。使用索引表来代替查找。

    每个类中都有一个虚方法表,表中存放着各个方法的实际入口。

i++和++i在JVM层面的不同

int i = 8; i = i++;

bipush 8就是把8压到操作数栈中。
istore_1就是操作数栈出栈,存到本地变量表的第1位置的i,就是代码中的i = 8;
iload_1,就是变量表中第一个位置的i压栈到操作数栈顶,此时栈顶为8
iinc 1 by 1,就是变量表中第一个位置的i加1,那么变量表最终i=9
istore_1,又把栈顶的8存回了变量表中的i,那么i=8; 因为java代码中又赋值给了i。

int i= 8; i = ++i;

0 bipush 8 把8压到操作数栈中
2 istore_1 操作数栈出栈,存到本地变量表的第1位置的i,就是代码中的i = 8;
3 iinc 1 by 1 变量表中第一个位置的i加1,那么变量表最终i=9
6 iload_1 变量表中第一个位置的i压栈到操作数栈顶,此时栈顶为9
7 istore_1 又把栈顶的9存回了变量表中的i,那么i=9;

泛型在继承上面的体现:

虽然类A是类B的父类,但是G《A》和G《B》二者不具备子父类关系,二者是并列关系。

补充:类A是类B的父类,A《G》是 B《G》 的父类

数组支持协变:

例如:Fruit[] fruit = new Apple[10];

但是仍然既能加入apple类,又可以加入orange类,在真正运行的时候会报错

泛型的容器只能通过引入通配符?进行协变和逆变:

List<? extends Fruit> flist = new ArrayList<Apple>();

但是不能添加,只能读

编译器不知道List<? extends Fruit>所持有的具体类型是什么,所以一旦执行这种类型的向上转型,你就将丢失掉向其中传递任何对象的能力。但是一定知道是fruit的类型,可以用fruit来接收。

List<? super Fruit> fruits = new ArrayList<Object>();

只能添加,不能读

编译器不会报错,因为fruits接受Fruit的基类类型,而该类型可以引用其子类型(多态性)

因为fruits限定的是下界是Friut类型,因此,编译器并不知道确切的类型是什么,没法找到一个合适的类型接受返回值,但是Object肯定可以。

我们把那些只能从中读取的对象称为生产者(Producer),我们可以从生产者中安全地读取;只能写入的对象称为消费者(Consumer)。

Hash算法:

一个优秀的hash算法的要求:

  • 从hash值不可以反向推导出原始的数据,经过映射后的数据和原始数据没有对应关系
  • 输入数据的微小变化会得到完全不同的hash值,相同的数据会得到相同的值
  • 哈希算法的执行效率要高效,长的文本也能快速地计算出哈希值
  • hash算法的冲突概率要小

可以通过以下几种方法,实现hash:

直接寻址法(关键字的线性函数),数字分析法(例如年月日的后几位变化大,采用后面的数字构成散列地址),平方取中法(关键字平方后的中间几位),折叠法(将关键字分割为位数相同的几部分,相加),随机数法(用一个随机函数,取关键值的随机值),除留余数法。

如何解决hash冲突:

开放地址(外加增量),再哈希,链地址法,建立公共溢出区。

HashMap:

  • hash=(h = key.hashCode()) ^(h >>> 16)

原因:这样可以运用到每一位上的数字,使之分散更均匀。

  • tab[i = (n-1)&hash]

相比于%更快,这也是容量为2的n次幂的原因(只有用2的n次幂才可以用此公式套用)。

  • 看懂HashMap的并发问题,也就间接证明了看过源码:

一、同时put时,其中一个线程的数据会被覆盖,这自不用说。

二、当有两个线程同时对其扩容时,在resize方法中

if ((e = oldTab[j]) != null) 
                oldTab[j] = null

他会将原来的节点置为空(方便gc),所以只有一个线程可以拿到数据,其他线程会丢失。

  • resize优化

1.7中扩容后仍旧会重新计算,key%新的长度值。(会有大量重复计算)

1.8中newCap和oldCap相比,只是将1左移动了一位。n-1同理

如果新增的那个bit位是0的话,就仍旧是原来的位置,如果是1的话,就+oldCap。

 do {
                        next = e.next;
     					//重点:如果==0,则代表那位新增的bit位是0
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {//否则是1
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        //注意这里
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        //注意这里
                        newTab[j + oldCap] = hiHead;
                    }

另一个优化点是,相比1.7,1.8从头插法改为了尾插法,不会出现并发成环的情况。头插法(会让原来的链表在扩容的时候倒置,两个线程同时对其倒置,会成为环。)

  • HashMap和红黑树、AVL的比较。(log N)

红黑树不追求完全平衡,只要求部分达到平衡,降低了旋转的要求,从而提高了性能,针对插入和删除节点,最多旋转3次可以实现恢复平衡,复杂度为O(1),其性能高于AVL.

实际应用中,搜索的次数远远大于插入和删除,则选择AVL,如果差不多,选择红黑树

红黑树特点:1、节点要么为黑色,要么为红色。2、根节点为黑色。3、叶子节点为黑色。4、每个红色节点的左右孩子都是黑色。

  • ArrayList

初始化赋值的时候是个空数组,当真正对数组添加元素时,扩充为10.

扩容时

newCapacity = oldCapacity + (oldCapacity >> 1) 偶数为原来的1.5倍,奇数为1.5倍左右
elementData = Arrays.copyOf(elementData, newCapacity);
  • HashSet的Value就是static的一个Object(与HashMap同理)

组合和继承

  • 继承(Is-a)

优点:容易形成新的实现;减少代码冗余;易于修改被复用的实现。

缺点:

父类的内部细节对子类可见,破坏了封装性。

子类从父类继承的方法在编译时刻就确定好了,无法在运行期间改变从父类继承的方法。

父类的方法做出修改的话,子类的方法也必须做出相应的修改。子类与父类是高耦合的。

  • 组合(has-a)

优点:

当前对象只能通过所包含的那个对象取调用其方法,所以所包含的对象的内部细节对当前对象不可见。

当前对象与包含的对象是一个低耦合的关系,如果修改包含对象的类中代码不需要修改当前对象的代码。

当前对象可以在运行时动态的绑定所包含的对象。可以通过set方法给其所包含的对象赋值

缺点:

容易产生过多的对象。

为了组合多个对象,必须仔细对接口定义。

  • 组合和继承的选择

组合相比继承更灵活和更稳定。优先使用组合

选择继承:

一种特殊的类(String,Object,包装类),不止是父类的一个角色。

子类的实例不需要变成另一个类的对象。(不需要动态绑定)

子类是父类的扩展

拆箱和装箱

遇运算符会拆箱,遇equals会装箱。

装箱是根据数值是int还是long,转化为对应的包装类。

-128到127有缓存。

if(i >= IntegerCache.low && i <= IntegerCache.high)
	return IntegerCache.cache[i+(-IntegerCache.low)];
return new Integer(i);

String类为final修饰的原因

1.为实现字符串常量池。(设计时认为,字符串复用带来的效率更高,节省更多的空间)

2.为了线程安全。

3.为了实现String可以创建HashCode的不可变性。HashCode被缓存后,不需要再更改。

  • 其余问题
String str1 = "abc";  // 在常量池中

String str2 = new String("abc"); // 在堆上

当直接赋值时,字符串“abc”会被存储在常量池中,只有1份,此时的赋值操作等于是创建0个或1个对象。如果常量池中已经存在了“abc”,那么不会再创建对象,直接将引用赋值给str1;如果常量池中没有“abc”,那么创建一个对象,并将引用赋值给str1。

new String(“abc”);

当JVM遇到上述代码时,会先检索常量池中是否存在“abc”,如果不存在“abc”这个字符串,则会先在常量池中创建这个一个字符串。然后再执行new操作,会在堆内存中创建一个存储“abc”的String对象,对象的引用赋值给str2。此过程创建了2个对象。

如果检索常量池时发现已经存在了对应的字符串,那么只会在堆内创建一个新的String对象,此过程只创建了1个对象。

  • intern方法会将字符串加入常量池。(有些字符串的创建,不会在字符串常量池中创建)

反射

  • 动态代理

是指客户通过代理类来调用其它对象的方法,并且是在程序运行时根据需要动态创建目标类的代理对象。

interface Human{
    String getBelief();
    void eat(String food);

}
//被代理类
class SuperMan implements Human{
    @Override
    public String getBelief() {
        return "I believe I can fly!";
    }
    @Override
    public void eat(String food) {
        System.out.println("我喜欢吃" + food);
    }
}
class HumanUtil{
    public void method1(){
        System.out.println("====================通用方法一====================");
    }
    public void method2(){
        System.out.println("====================通用方法二====================");
    }
}
class ProxyFactory{
    //调用此方法,返回一个代理类的对象。解决问题一
    public static Object getProxyInstance(Object obj){//obj:被代理类的对象
        MyInvocationHandler handler = new MyInvocationHandler();
		handler.bind(obj);
		return Proxy.newProxyInstance(obj.getClass().getClassLoader(),obj.getClass().getInterfaces(),handler);
    }
}
class MyInvocationHandler implements InvocationHandler{
    private Object obj;//需要使用被代理类的对象进行赋值
    public void bind(Object obj){
        this.obj = obj;
    }
    //当我们通过代理类的对象,调用方法a时,就会自动的调用如下的方法:invoke()
    //将被代理类要执行的方法a的功能就声明在invoke()中
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        HumanUtil util = new HumanUtil();
        util.method1();
        //method:即为代理类对象调用的方法,此方法也就作为了被代理类对象要调用的方法
        //obj:被代理类的对象
        Object returnValue = method.invoke(obj,args);
        util.method2();
        //上述方法的返回值就作为当前类中的invoke()的返回值。
        return returnValue;
    }
}

public class ProxyTest {
    public static void main(String[] args) {
        SuperMan superMan = new SuperMan();
        //proxyInstance:代理类的对象
        Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan);
        //当通过代理类对象调用方法时,会自动的调用被代理类中同名的方法
        String belief = proxyInstance.getBelief();
        System.out.println(belief);
        proxyInstance.eat("四川麻辣烫");
        System.out.println("*****************************");
        NikeClothFactory nikeClothFactory = new NikeClothFactory();
        ClothFactory proxyClothFactory = (ClothFactory) ProxyFactory.getProxyInstance(nikeClothFactory);
        proxyClothFactory.produceCloth();

    }
}

  //方法一
  Class clazz = Class.forName("com.java.Person");
  //方法二
  ClassLoader classLoader = ReflectionTest.class.getClassLoader();
  Class clazz = classLoader.loadClass("com.java.Person");

forName会对类进行解释,初始化class,将静态变量初始化,执行静态代码块的内容,而loadClass则只是将.class文件加载到JVM中,只有在第一次newInstance的时候会初始化。

单例模式

  • 懒汉式
class Bank{

    private Bank(){}

    private volatile static Bank instance = null;

    public static Bank getInstance(){
        //方式一:效率稍差
//        synchronized (Bank.class) {
//            if(instance == null){
//
//                instance = new Bank();
//            }
//            return instance;
//        }
        //方式二:效率更高
        if(instance == null){

            synchronized (Bank.class) {
                if(instance == null){

                    instance = new Bank();
                }

            }
        }
        return instance;
    }

}

使用volatile的作用(禁止指令重排)

instance = new Bank();

此段代码分三步执行:

1、为instance分配内存空间。2、初始化instance。3、将instance指向分配的内存地址。

但是由于指令重排(编译器优化的重排序,在不改变单线程的语义下),会变为1->3->2,这使得在多线程的情况下,可能返回一个没有被初始化的实例对象。

杂七杂八

  • 异常处理就是为了在程序发生错误的时候可以继续运行不至于崩溃

  • 标识符:由26个英文字母大小写,0-9,$或_组成,数字不可以开头。

  • 二机制:0b 八进制:0 十六进制:0x

  • 方法重载只和参数的个数和类型有关

  • 面向过程:强调的是功能行为,以函数为最小单位,考虑怎么做。
    面向对象:强调具备了功能的对象,以类/对象为最小单位,考虑谁来做。

  • finalize:无法确定方法什么时候执行,一般不调用。Java语言规范并不保证finalize方法会被及时地执行、而且根本不会保证它们会被执行。

  • 抽象类是模板式设计,接口是一种辐射式设计。(谁需要这个功能,就实现这个接口)

  • java字节码是伪机器码,自然会比解析型语言高,JVM不是解析型语言,是半编译半解析型语言,解析型语言没有编译过程,直接解析源代码文本,相当于是在执行时进行了一次编译,而java的字节码虽然无法和本地机器码完全一一对应,但可以简单映射到本地机器码,无需做复杂的语法分析之类的编译处理,当然比纯解析语言快。
    解释型语言是运行时才编译成机器语言,自然效率低。java由字节码到机器语言效率高一点,另外jvm还会将热点代码缓存。

举报

相关推荐

0 条评论