美文网首页面试题Android面试宝典app开发
JAVA面试-基础加强与巩固:反射、注解、泛型等

JAVA面试-基础加强与巩固:反射、注解、泛型等

作者: 小楠总 | 来源:发表于2016-09-18 15:56 被阅读2630次

    作者-焕然一璐,支持原创,转载请注明出处,谢谢合作。
    原文链接:http://www.jianshu.com/p/aaf8594e02eb

    企业重视的是学习能力:基础很重要

    JDK1.5新特性

    1. 泛型
    2. foreach
    3. 自动拆箱装箱
    4. 枚举
    5. 静态导入(Static import)
    6. 元数据(Metadata)
    7. 线程池
    8. 注解

    JDK1.6新特性

    1. Desktop类和SystemTray类
    2. 使用JAXB2来实现对象与XML之间的映射
    3. StAX
    4. 使用Compiler API
    5. 轻量级Http Server API
    6. 插入式注解处理API(Pluggable Annotation Processing API)
    7. 用Console开发控制台程序
    8. 对脚本语言的支持
    9. Common Annotations

    JDK1.7新特性

    1 对集合类的语言支持;
    2 自动资源管理;
    3 改进的通用实例创建类型推断;
    4 数字字面量下划线支持;
    5 switch中使用string;
    6 二进制字面量;
    7 简化可变参数方法调用。

    JDK1.8新特性

    1. 接口的默认方法,也就是接口中可以有实现方法
    2. 函数式接口与静态导入
    3. Lambda 表达式
    4. 访问局部变量

    静态导入:JDK1.5新特性

    //静态导入某一个方法
    import static java.lang.Math.abs;
    //静态导入一个类的所有静态方法
    import static java.lang.Math.*;
    
    public static void main(String[] args) {
        //方法可以直接使用
        System.out.println(abs(-100));
    }
    
    
    //如果类里面原本就有同名的方法的话,就会覆盖掉静态导入的方法了
    public static void abs() {
    
    }
    

    可变参数

    没有可变参数之前,实现不定长参数只能通过方法的重载实现,但是这样做工作量很大。

    没有可变参数之前,实现可变参数的方法可以通过Obiect[]来实现。

    1. 可变参数只能放在参数列表的最后一个

    2. 编译器隐含为可变参数创建数组,因此可以通过数组的方式去使用可变参数

       public static int add(int x, int... args) {
           int sum = x;
      
           for (int i = 0; i < args.length; i++) {
               sum += args[i];
           }
      
           return sum;
       }
      

    增强for循环

        //数组或者实现了Iterable接口的对象
        for 修饰符 Type arg : 要迭代的对象) {
            
        }
    

    基本数据类型的自动装箱与拆箱

        //自动装箱示例,自动将基本数据类型包装成对象
        Integer i1 = 1000;
        Integer i2 = 1000;
    
        //自动拆箱示例,自动将对象解包成基本数据类型
        System.out.println(i1 + 10);
    
    
        //如果数值在-128到127之前,对象会复用(享元设计模式)
        System.out.println(i1 == i2);
    

    -128到127会缓冲起来,节省内存。这是享元设计模式的应用,内部状态/外部状态(可变)

    枚举

    为什么要使用枚举?

    比如,我们要使用1-7代表星期一到星期天,那么通常我们会想到的做法做,定义一个类,并且提供一些公有的静态常量,例如:

    public class WeekDay {
        public static final int SUN = 1;
        public static final int MON = 2;
    }
    

    但是当我们使用的时候,有些人可能不想去理会这个细节,比如会直接传入1(可能他自己觉得1是代表星期一的),因此运行的时候就会出现一些意想不到的问题。

    为了解决这个问题,java 5重新引入枚举,保证变量的值只能取指定值中的某一个,通过在代码编译的时候就可以知道传的值是否合法。

    枚举的模拟:

    /**
     * 自己手动实现枚举类
     * 1. 构造方法必须是私有的
     * 2. 提供公有的静态成员变量代表枚举,并且通过匿名内部子类去生成对象
     * 3. 对外提供的方法必须是抽象的
     */
    public abstract class WeekDay {
    
        public static WeekDay MON = new WeekDay(0) {
            @Override
            public WeekDay next() {
                return SUN;
            }
    
            @Override
            public String toString() {
                return "SUN";
            }
    
        };
    
        public static WeekDay SUN = new WeekDay(1) {
            @Override
            public WeekDay next() {
                return MON;
            }
    
            @Override
            public String toString() {
                return "MON";
            }
        };
    
        private int num;
    
        //私有构造方法
        private WeekDay() {
    
        }
    
        //在生成匿名内部类的时候,可以传参给父类的构造函数
        private WeekDay(int num) {
            this.num = num;
        }
    
        //对外提供的抽象方法,由子类去实现
        public abstract WeekDay next();
    
    }
    

    一些关键的点已经在上面的注释中给出,在使用的时候,我们只能通过这样去生成WeekDay对象。(实际上枚举内部也是生成对象嘛)

    WeekDay weekDay = WeekDay.MON;
    weekDay.next();
    

    枚举的实现

    public enum WeekDay {
    
        //枚举对象必须放在最前面,匿名内部类的创建可以带参数,必须实现父类的抽象方法
        MON(1) {
            public WeekDay next() {
                return SUN;
            }
        },
    
        SUN(2) {
            public WeekDay next() {
                return MON;
            }
        };
    
        private int num;
    
        //枚举的构造函数是默认为private的,可以带参数
        WeekDay(int num) {
            this.num = num;
        }
    
        public abstract WeekDay next();
    
    }
    

    枚举使用,以及一些枚举特有的方法:

    //使用方法,跟一般的对象是一模一样的
    WeekDay w = WeekDay.MON;
    //直接打印枚举对象实际上是调用了toString
    System.out.println(w);
    //打印枚举的名字,实际上打印类的简短的名字w.getClass().getSimpleName()
    System.out.println(w.name());
    //打印枚举对象在枚举中的位置,0开始算
    System.out.println(w.ordinal());
    //通过字符串去或者获取(构造)枚举对象
    System.out.println(WeekDay.valueOf("MON"));
    
    //获取枚举类的所有对象,通过数组的方式去遍历
    for (WeekDay value : WeekDay.values()) {
        System.out.println(value);
    }
    

    枚举的特殊用法---利用枚举可以简简单单就实现单例模式

    反射 -- JDK1.2就有了

    了解反射的基础--Class类

    用来描述Java类的类就是Class这个类。

    每个类在java虚拟机中占用一片内存空间,里面的内容就是对应这个类的字节码(Class)

    Class字节码的获取:

    类名.class
    对象.getClass
    Class.forName("类的全名");
    

    其中,跟反射密切相关的就是forName这个方法,通过类名去获取字节码。前面两种都是虚拟机中已经加载过了。forName方法在当虚拟机中没有加载过对应字节码的时候,就会去动态地加载进来;当已经加载过的时候,直接复用加载过的。

    九种预定义好的基本Class字节码

    八种数据类型,外加void也有对应的字节码。下面给出一些例子:

    Class<Integer> type = Integer.TYPE;
    Class<Integer> integerClass = int.class;
    Class<Void> voidClass = Void.class;
    

    例子:

    public static void main(String[] args) throws Exception {
        
        Class<?> c1 = Class.forName("java.lang.String");
        Class<String> c2 = String.class;
        Class<? extends String> c3 = new String("123").getClass();
    
        //返回的都是同一份字节码,因此==
        System.out.println(c1 == c2);
        System.out.println(c2 == c3);
    
        //判断是不是基本类型(九种)
        System.out.println(int.class.isPrimitive());
        System.out.println(int[].class.isPrimitive());
        
        //判断是不是数组(数组也是一种类型,所有类型都可以反射)
        System.out.println(int[].class.isArray());
        
    }
    

    反射的概念

    一句话总结:反射就是把Java类中的各种成分通过java的反射API映射成相应的Java类,得到这些类以后就可以对其进行使用。比如方法,构造方法,成员变量,类型,包等。下面分别取讲解。

    Constructor类

    得到所有的构造方法

    Constructor<?>[] constructors = Class.forName("java.lang.String").getConstructors();
    

    得到指定参数的某一个构造方法:

    Constructor<?> constructor = Class.forName("java.lang.String").getConstructor(StringBuffer.class);
    

    创建对象

    String s = (String) constructor.newInstance(new StringBuffer("abc"));
    System.out.println(s.charAt(2));
    
    也可以直接调用无参数的构造,实际上也是先找到Constructor再调用Constructor的newInstance
    String s = (String) Class.forName("java.lang.String").newInstance();
    

    查看源码可以发现,Class的newInstance方法中有把Constructor缓存起来的。因为反射的使用会大大降低系统的性能,对于计算机来说比较耗时,而且也容易发生运行时异常,因此需要慎重使用。

    Field对象

    Field代表类中的一个成员变量

    例如我们有一个测试的类Point

    public class Point {
    
        public int x;
        private int y;
        protected int z;
    
        public Point(int x, int y, int z) {
            this.x = x;
            this.y = y;
            this.z = z;
        }
    }
    

    那么,我们可以利用发射机制去得到虚拟机中某个对象的成员变量,并且获取他们的值

    Point point = new Point(1, 2, 3);
    
    //通过反射拿到成员变量的值
    Field x = point.getClass().getField("x");
    System.out.println(x.get(point));
    
    //如果是私有或者保护变量的话,不能拿到Field,会报找不到Field的错误
    //Field y = point.getClass().getField("y");
    
    //私有变量需要通过使用getDeclaredField去获取,如果要获取值的话,需要设置setAccessible为true
    //这样的反射比较暴力
    Field y = point.getClass().getDeclaredField("y");
    y.setAccessible(true);
    System.out.println(y.get(point));
    

    例子,把对象中所有String类型的值中的a修改为*:

    public class Reflecttest1 {
    
        public static void main(String[] args) throws Exception {
            Test test = new Test();
            changeString(test);
            System.out.println(test);
        }
    
        private static void changeString(Object obj) throws Exception {
            for (Field field : obj.getClass().getFields()) {
                
                //判断是不是String类型,注意这里最好使用==
                if (field.getType() == String.class) {
                    String oldV = (String) field.get(obj);
                    String newV = oldV.replace('a', '*');
                    field.set(obj, newV);
                }
            }
        }
    }
    

    Method类

    String a = "0123";
    //获取方法,并且调用
    Method charAt = a.getClass().getMethod("charAt", int.class);
    //通过反射调用方法,第一个参数是对象,如果为静态方法的话,应该传null
    //第二个参数是可变长参数,传入的是实参
    System.out.println(charAt.invoke(a, 1));
    

    调用指定对象的main方法,注意其中传递字符串数组的问题

    由于可变长参数的问题,jdk为了兼容1.4以下的版本,会把传进去的数组进行拆包。因此注释代码会报参数不匹配的错。

    解决方案是

    1、再利用Object数组包装一层,告诉编译器,可以拆包,但是拆开之后就是一个String数组。

    2、相强制转换为Object对象,告诉编译器,不要拆包。

    public class ReflectTest3 {
    
        public static void main(String[] args) throws Exception {
    
            Method mainMethod = Class.forName("com.nan.test.T").getMethod("main", String[].class);
    //        mainMethod.invoke(null, new String[]{"123"});
            mainMethod.invoke(null, new Object[]{new String[]{"123"}});
            mainMethod.invoke(null, (Object) new String[]{"123"});
    
        }
    
    }
    
    class T {
        public static void main(String[] args) {
            for (String s : args) {
                System.out.println(s);
            }
        }
    }
    

    数组类型

    具有相同的维度以及元素类型的数组,属于同一种Class类型。

    例子:

    int[] a1 = new int[3];
    int[] a2 = new int[4];
    int[][] a3 = new int[3][4];
    String[] a4 = new String[3];
    
    System.out.println(a1.getClass());
    System.out.println(a2.getClass());
    System.out.println(a3.getClass());
    System.out.println(a4.getClass());
    

    输出结果:

    class [I
    class [I
    class [[I
    class [Ljava.lang.String;
    

    其中[代表是数组类型,I代表元素类型(int)

    八种基本类型的数组不能转换成Object数组。因此下面的语句不合法:

    Object[] objects = a1;
    
    //因为基本类型的一维数组不能当成Object数组,因此只能当做是一整个数组对象
    //因此打印出来的结果是数组的对象的值,而不是里面的内容
    //那么,按照JDK1.5的可变长参数语法,只能解析成一个数组对象
    System.out.println(Arrays.asList(a1));
    //String类型可以转换成Object数组打印的是内容
    System.out.println(Arrays.asList(a4));
    

    数组

    可以通过反射来对数组进行操作,因为是Object数组,因此不能确定整个数组是同一个类型,因此只能确定的是每一项的类型。

    private static void printObj(Object o) {
        //判断是不是数组类型
        if (o.getClass().isArray()) {
            //通过反射APIArray去遍历数组
            int length = Array.getLength(o);
            for (int i = 0; i < length; i++) {
                Object item = Array.get(o, i);
                System.out.println(item);
            }
        } else {
            System.out.println(o);
        }
    }
    

    equals与hashCode的联系与区别。

    1. 一个类的两个实例对象equals的时候,hashCode也必须相等,反之不成立。
    2. hashCode只有在hash集合中才有意义,比如hashMap、hashSet等。当对象被存入hash集合或者从hash集合中移除、contains方法调用等的时候,先会通过计算hash值,算出对象应该在的存储区域,然后再通过在这块存储区域,里面通过equals方法去判断对象是否重复。(hash相等,equals可以不相等)

    一般来说,两个都需要重写,而且在对象插入了hash集合以后,不要再修改这个对象与hash计算有关的数值了,因为这样会导致hash集合根据变化之后的hash值找不到这个对象了,对象不能被清理,从而造成内存泄漏。

    HashSet<Point> set = new HashSet<>();
    Point p0 = new Point(1,2,3);
    Point p1 = new Point(1,2,3);
    
    set.add(p0);
    //如果重写了hashCode方法,这个时候不重写equals方法,那么这个对象可以被插入
    //如果重写了hashCode以及equals方法,那么这个对象不可以被插入
    set.add(p1);
    
    //数值改变了,对象不能被移除(找到),从而造成内存泄漏
    p0.x = 10000;
    set.remove(p0);
    
    System.out.println(set.size());
    

    而一般的非hash集合,例如ArrayList,只保存数据的引用,数据是可以重复的。

    Java反射的作用 -- 实现框架功能

    框架与工具的区别:

    相同:都是其他人提供的

    不同点:

    1. 工具类是被用户调用
    2. 框架是调用用户提供的类

    例如:

    1. 框架提供配置文件,用户可以配置,例如类名
    2. 读取用户的配置文件
    3. 通过反射加载对应的类,并且动态去使用

    配置文件的读取

    //一定要用完整的路径,不是硬编码,而是运算出来的
    InputStream is = new FileInputStream("文件目录");
    Properties properties = new Properties();
    properties.load(is);
    
    String value = properties.getProperty("key");
    

    一般配置文件的加载基本都是利用类加载器来加载。

    //通过类加载器可以把与类放在一起的配置文件读取出来,这里是与类相对路径。如果写上/代表是绝对路径,需要写完整/包名。。。。。
    InputStream res = ReflectTest3.class.getResourceAsStream("文件目录");
    //InputStream res = ReflectTest3.class.getClassLoader().getResourceAsStream("文件目录");
    properties.load(res);
    

    内省,与java Bean

    java bean的概念:符合一定get、set规则的java类

    java bean的属性名为get、set方法去掉get、set前缀,剩下的-- 如果第二个字母也是小写的话,那么 -- 首字母需要变成小写

    例如:

    getAge-->age
    
    setCPU-->CPU
    

    内省:操作Java Bean对象的API

    Bean b = new Bean(1);
    
    String propertyName = "x";
    
    //普通方法x-->X-->getX-->调用
    //下面使用内省的方法去获取get方法,set方法也是同一个道理
    PropertyDescriptor pd = new PropertyDescriptor(propertyName, b.getClass());
    Method getXMethod = pd.getReadMethod();
    System.out.println(getXMethod.invoke(b, null));
    

    比较麻烦的写法,通过BeanInfo去做

    BeanInfo beanInfo = Introspector.getBeanInfo(b.getClass());
    PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
    for (PropertyDescriptor p : pds) {
        if (p.getName().equals("x")) {
            Method readMethod = p.getReadMethod();
            readMethod.invoke(b, null);
        }
    }
    

    注解 Annotation

    小知识:

    1. 类型名:形容词+名词
    2. 方法名:动词+名词

    注解的概念

    注解实际上是一个类,写注解实际上是创建注解的对象。注解相当于为程序打一种标记。javac编译工具、开发工具以及其他程序可以利用反射来了解你的类以及各种元素上有没有何种标记,就可以去做相应的事。

    标记可以加在包、类型(Type,包括类,枚举,接口)、字段、方法、方法的参数以及局部变量上面。

    下面是java的一些比较常见的注解:

    Deprecated              //标记方法过时
    Override                //标记该方法是子类覆盖父类的方法,告诉编译器去检查
    SuppressWarnings        //去掉过时的删除线
    

    注解的应用结构图

    一共需要三个类:

    例子:

    这是一个注解:

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * Created by Administrator on 2016/9/16.
     */
    
    @Retention(RetentionPolicy.RUNTIME)//注解保留在哪个声明周期
    @Target({ElementType.METHOD, ElementType.TYPE})//作用于什么元素身上
    public @interface A {
    
    }
    

    注解作用的类:

    @A
    public class AnnotationTest {
    
    }
    

    获取注解(通过对AnnotationTest进行反射):

    Class<AnnotationTest> c = AnnotationTest.class;
    //判断是否有对应的注解
    if (c.isAnnotationPresent(A.class)) {
        //获取注解
        A a = c.getAnnotation(A.class);
    }
    

    元注解,用来给注解添加注解的注解

    @Target(ElementType.METHOD)         //声明注解可以作用在什么地方,特别注意TYPE是类、枚举、接口身上
    @Retention(RetentionPolicy.SOURCE)  //声明注解是在哪个声明周期的,分别是源代码,class文件,运行时
    

    注意,多个属性的话需要用大括号括起来:

    @Target({ElementType.METHOD, ElementType.TYPE})
    

    注解的生命周期

    1. SOURCE:注解被保留到源文件阶段。当javac把源文件编译成.class文件的时候,就将相应的注解去掉。例如常见的Override、SuppressWarnings都属于SOURCE类型的生命周期,因为一旦代码编译之后该注解就没用了。
    2. CLASS:java虚拟机通过类加载器向内存中加载字节码的时候,就将相应的注解去掉,因此无法通过反射获取相应的注解。
    3. RUNTIME:注解保留在内存中的字节码上面了,虚拟机在运行字节码的时候,仍然可以使用的注解。例如Deprecated,类被别人使用的时候,加载到内存,扫描,从二进制代码去看是否过时,而不是检查源代码。

    为注解增加属性

    注解参数的可支持数据类型:

    1. 所有基本数据类型(int,float,boolean,byte,double,char,long,short)
    2. String类型
    3. Class类型
    4. enum类型
    5. Annotation类型
    6. 以上所有类型的数组

    例子:

    @Retention(RetentionPolicy.RUNTIME)//注解保留在哪个声明周期
    @Target({ElementType.METHOD, ElementType.TYPE})//作用于什么元素身上
    public @interface A {
    
        String stringAttr();
    
        Class value() default Object.class;
    
        int[] arrAttr();
    
        Deprecated annoAttr() default @Deprecated;
    }
    

    在使用注解的时候注意点:

    1. 当只有value需要设置值的时候(即只有value属性或者其他属性已经制定了default的时候),可以直接不写value,直接在括号里面写值,例如:

      @SuppressWarnings("deprecation")

    2. 当类型是数组的时候,如果元素只有一个,那么可以省略大括号,直接写一个元素即可。

    通过反射获得注解之后,就可以随心去使用了:

    Class<AnnotationTest> c = AnnotationTest.class;
    if (c.isAnnotationPresent(A.class)) {
        A a = c.getAnnotation(A.class);
        //获得stringAttr属性的值
        System.out.println(a.stringAttr());
    }
    

    泛型

    概念

    集合,反射等等地方都使用到了泛型,免去了强制类型转换的不安全性问题,包括code阶段以及运行阶段。泛型是给编译器看的,让编译器拦截源程序中的非法输入,编译完以后就会去掉类型信息,保证程序的运行效率。对于参数化的泛型类型,getClass方法的返回值和原始类型完全一样。

    所以编译完以后,跳过编译器,通过反射就可以向集合添加其他类型的数据,例子如下:

    List<Integer> list = new ArrayList<>();
    //通过反射的方式取添加“非法类型”到集合当中
    list.getClass().getMethod("add", Object.class).invoke(list, "abc");
    System.out.println(list.get(0));
    

    关于泛型的一些术语:

    ArrayList<E>                        泛型类型
    ArrayList<E>中的E                 类型变量或者类型参数
    ArrayList<String>                   参数化的类型
    ArrayList<String>中的String           实际类型参数
    ArrayList<String>中的<>读作type of
    
    1. 用不用泛型,程序最终的运行结果都一样,用了有好处而已
    2. 参数化类型,不考虑类型参数的继承关系

    例如,下面的这行代码是错误的,因为不考虑父子关系:

    List<Object> list1 = new ArrayList<String>();
    

    泛型中的通配符 ?

    不用Object,而是使用?表示任意类型。?通配符可以引用各种其他参数化的类型,?通配符定义的变量主要用作引用。类型参数没有赋值的时候,不能调用与类型参数有关的方法(方法中有泛型参数的方法)。

    例子:

    public static void printSise(List<?> l) {
    
        l.add(new Object());//这一局编译就报错
        Object o = l.get(1);//返回值有泛型,但是我们可以转换为Object
    
        for (Object obj : l) {
            System.out.println(obj);
        }
    }
    

    泛型的上下边界,可以用&实现多个接口的限定

    //上边界
    List<? extends Number> l1 = new ArrayList<Integer>();
    
    //下边界
    List<? super Integer> l2 = new ArrayList<Object>();
    

    泛型的案例,以及各种集合(主要是MAP)的迭代方法:

    1.增强for循环遍历MAP,这是最常见的并且在大多数情况下也是最可取的遍历方式。注意:for-each循环在java 5中被引入所以该方法只能应用于java 5或更高的版本中。如果你遍历的是一个空的map对象,for-each循环将抛出NullPointerException,因此在遍历前你总是应该检查空引用。

    Map<Integer, Integer> map = new HashMap<Integer, Integer>();
    
    for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
    
        entry.getKey();
        entry.getValue();
    
    }
    

    2.在for-each循环中遍历keys或values.如果只需要map中的键或者值,你可以通过keySet或values来实现遍历,而不是用entrySet。该方法比entrySet遍历在性能上稍好(快了10%),而且代码更加干净。

    Map<Integer, Integer> map = new HashMap<Integer, Integer>();

    //遍历map中的键
    
    for (Integer key : map.keySet()) {
    
    }
    
    //遍历map中的值
    
    for (Integer value : map.values()) {
    
    }
    

    3.使用迭代器,这种方法可以在迭代之中删除元素。

    Map<Integer, Integer> map = new HashMap<Integer, Integer>();
    
    Iterator<Map.Entry<Integer, Integer>> entries = map.entrySet().iterator();
    
    while (entries.hasNext()) {
    
        Map.Entry<Integer, Integer> entry = entries.next();
    
        entry.getKey();
        entry.getValue());
    
    }
    

    4.通过键值去遍历,效率低下,一般不采用。

    Map<Integer, Integer> map = new HashMap<Integer, Integer>();
    
    for (Integer key : map.keySet()) {
    
        Integer value = map.get(key);
    
        System.out.println("Key = " + key + ", Value = " + value);
    
    }
    

    总结:

    如果仅需要键(keys)或值(values)使用方法二。如果你使用的语言版本低于java 5,或是打算在遍历时删除entries,必须使用方法三。否则使用方法一(键值都要)。

    由C++的模板函数,引入自定义泛型

    java中的泛型不能完全做到C++中的模板功能的原因:

    java中的泛型类似于C++中的模板,但是这种相似性仅限于表面,java中的泛型基本上是在编译器中实现,用于编译器执行类型检查和推断,然后生成普通的非泛型字节码,这种技术成为擦除。因为扩展虚拟机指令集来支持泛型被认为是无法接受的,这会为java厂商升级JVM造成困难。因此,泛型参数不同不能构成重载。

    1. 泛型的实际类型不能是基本类型,只能是引用类型。
    2. 修饰符之后,返回值类型之前,用<T>声明一种新的类型。
    3. 可以有多个类型变量,用逗号隔开。
    4. 类型参数的名字一般用一个大写字母来命名。
    5. 编译器不允许直接new T的数组或者对象。
    6. 可以用类型变量表示异常,称为参数化异常,用得不多。

    例子:

    private static <T> T[] swap(T[] arr, int i, int j) {
        T tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
        return arr;
    }
    

    参数的类型推断(比较复杂)

    类型参数的类型推断:编译器判断泛型方法的实际类型参数的过程称为类型推断,类型推断是相对于知觉推断的,其实现方法是一种非常复杂的过程。根据调用泛型方法时实际传递参数类型或返回值的类型来推断,具体规则如下:

    1. 当某个类型变量值在整个参数列表中的所有参数和返回值中的一处被应用类,那么根据调用方法时该处的实际应用类型来确定,这很容易凭着感觉推断出来,即直接根据调用方法时传递的参数类型或返回值来决定泛型参数的类型,例如:
      swap(new String[3],3,4) --> static <E> void swap(E[] a,int i,int j)
    2. 当某个类型变量在整个参数列表中的所有参数和返回值中的多处被应用了,如果调用方法时这多处的实际应用类型都对应同一种类型来确定,这很容易凭着感觉推断出来,例如:
      add(2,5) -->static <T> T add (T a, T b)
    3. 当某个类型变量在整个参数列表中的所有参数和返回值中的多处被应用了,如果调用方法时这多处的实际应用类型对应到类不同的类型,且没有使用返回值,这时候取多个参数中的最大交集类型,例如,下面语句实际对应的类型就是Number了,编译没问题,只是运行时出问题:
      fill(new Integer[3],3.5f)-->static <T> void fill(T[], T v)//Integer∩Float = Number ,它们都是Number的子类
    4. 当某个类型变量在整个参数列表中的所有参数和返回值中的多处被应用了,如果调用方法时这多处的实际应用类型对应到了不同的类型,并且使用返回值,这时候优先考虑返回值的类型,例如,下面语句实际对应的类型就是Integer了,编译将报告错误,将变量x的类型改为float,对比eclipse报告的错误提示,接着再将变量x类型改为Number,则没有了错误:
      int x = add(3,3.5f) -->static <T> T add(T a,T b)

    定义泛型的类型

    1. 方法级别(上面已经讲过)

    2. 泛型的类型(类):多个方法使用的是同一个类型

      class A <T>{

      }

    注意,类里面的静态方法不能含有对象的泛型。但是可以有一般的泛型静态方法。例子:

    public class A<T> {
        //编译器报错,因为静态方法可以避开对象的创建嘛
        public static void add(T t) {
    
        }
    
        //编译器不报错,单独分开
        public static <E> void set(E e) {
    
        }
    }
    

    难点:通过反射的方法把方法中的参数的类型提取出来

    public static void main(String[] args) throws Exception {
    
        //通过反射的方法把方法中的参数的类型提取出来
        Method method = A.class.getMethod("test", List.class);
        //其返回的是参数的参数化的类型,里面的带有实际的参数类型
        Type[] types = method.getGenericParameterTypes();
        //将类型向参数化类型转换
        ParameterizedType type = (ParameterizedType) types[0];
        //得到原始类型(interface java.util.List)
        System.out.println(type.getRawType());
        //得到实际参数类型的类型(class java.lang.String)
        System.out.println(type.getActualTypeArguments()[0]);
    
    }
    
    
    public static void test(List<String> list) {
    
    }
    

    类加载器的介绍

    类加载器的父子关系以及管辖范围

    例子:获取并且打印类加载器:

    public static void main(String[] args) {
    
        //打印出当前线程的类加载器
        System.out.println(Thread.currentThread().getContextClassLoader().getClass().getName());
    
        //第一个类默认由当前线程的类加载器去进行加载
        System.out.println(ClassLoaderTest.class.getClassLoader().getClass().getName());
        //System是由BootStrap类加载器加载的,是C/C++写的,BootStrap不需要其他加载器去加载
        //在java层面不能获取该类的引用
        System.out.println(System.class.getClassLoader() == null);
    
    }
    

    类加载器的委托机制,相当于android中的事件传递,防止字节码的重复加载。

    自定义加载器

    原理:ClassLoader有loadClass和findClass两个方法,loadClass会首先去找父加载器,如果找不到就会回传,如果传到自己的话,就会回调findClass方法来加载class。为了保证这一个流程不被破坏(模板方法设计模式),因此我们需要覆盖的是findClass方法。

    自定义加载器可以实现字节码的加密解密

    下面仅仅写出一些关键的步骤:

    1. 写一个需要被加密的类,并且编译生成.class文件

    2. 利用加密算法(比如与0xff异或)对.class文件文件进行加密,用输入流读进来,再用输出流输出到文件中。

    3. 自定义类加载器,继承ClassLoader。复写findClass方法,把加密过后的.class加载进来,转换成字节数组并且解密,利用ClassLoader的下面这个方法把class文件转换成字节码。

    4. 得到字节码就可以通过反射的方式进行newInstance等使用了。

       //得到class文件转换成字节码
       protected final Class<?> defineClass(byte[] b, int off, int len)
      

    代理

    要为已存在的多个具有相同接口的目标类(已经开发好,或者没有源码)的各个方法增加一些系统功能,例如异常处理、日志、计算方法的运行时间、事务管理等。可以使用代理,代理就有这样的好处。

    JVM可以在运行期间动态生成出类的字节码,这种动态生成的类往往用作代理,成为动态代理。JVM生成的类必须实现一个或者多个接口,所以JVM生成的类智能用作具有相同家口的目标类的代理。

    CGLIB库可以动态生成一个类的子类,一个类的子类也可以用作该类的代理类,所以如果要为一个没有实现接口的类生成动态代理类的话,可以使用这个库。

    AOP面向切面编程的概念

    面向切面编程(AOP是Aspect Oriented Program的首字母缩写),在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。

    一般而言,我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。
    这样看来,AOP其实只是OOP的补充而已。OOP从横向上区分出一个个的类来,而AOP则从纵向上向对象中加入特定的代码。有了AOP,OOP变得立体了。如果加上时间维度,AOP使OOP由原来的二维变为三维了,由平面变成立体了。从技术上来说,AOP基本上是通过代理机制实现的。
    AOP在编程历史上可以说是里程碑式的,对OOP编程是一种十分有益的补充。

    例子

    使用反射API,JVM动态生成Collection接口的代理类

    //使用反射API,JVM动态生成Collection接口的代理类
    Class<?> clazzProxy0 = Proxy.getProxyClass(Collection.class.getClassLoader(), Collection.class);
    
    //输入这个动态生成的类的名字
    System.out.println(clazzProxy0.getName());
    
    //输出所有构造方法
    printConstructor(clazzProxy0);
    
    //输出所有方法
    printMethod(clazzProxy0);
    
    //使用这个类去构建对象
    //不能使用无参数的构造函数,因为代理类只有一个有参数的构造函数
    //clazzProxy0.newInstance();
    
    Constructor<?> constructor = clazzProxy0.getConstructor(InvocationHandler.class);
    Collection collection = (Collection) constructor.newInstance(new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            return null;
        }
    });
    

    生成代理类对象需要传入InvocationHandler对象,代理类的方法调用会触发InvocationHandler的分发,InvocationHandler内部会对被代理类的对象的方法进行调用,并且插入一些指定的功能。

    下面是打印所有构造方法与方法的函数

    private static void printMethod(Class<?> clazz) {
        System.out.println("所有方法");
        for (Method m : clazz.getMethods()) {
            StringBuilder builder = new StringBuilder(m.getName());
            builder.append("(");
    
            Class[] types = m.getParameterTypes();
            for (Class<?> t : types) {
                builder.append(t.getName()).append(",");
            }
            if (types.length != 0) {
                builder.deleteCharAt(builder.length() - 1);
            }
    
            builder.append(")");
    
            System.out.println(builder.toString());
        }
    }
    
    private static void printConstructor(Class<?> clazz) {
        System.out.println("所有构造方法");
        for (Constructor c : clazz.getConstructors()) {
            StringBuilder builder = new StringBuilder(c.getName());
            builder.append("(");
    
            Class[] types = c.getParameterTypes();
            for (Class<?> t : types) {
                builder.append(t.getName()).append(",");
            }
            if (types.length != 0) {
                builder.deleteCharAt(builder.length() - 1);
            }
    
            builder.append(")");
    
            System.out.println(builder.toString());
        }
    }
    

    一步到位:

    //一步到位,获取代理类并且生成对象
    Collection collection1 = (Collection) Proxy.newProxyInstance(Collection.class.getClassLoader(), new Class[]{Collection.class}, new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            return null;
        }
    });
    

    下面给出实用的案例,在每个方法的调用的时候插入广告:

    /**
     * Created by Administrator on 2016/9/18.
     */
    public class ProxyTest1 {
    
        //被代理的对象
        private static ArrayList<String> target = new ArrayList<>();
    
        public static void main(String[] args) {
    
            //一步到位,获取代理类并且生成对象
            Collection collection = (Collection) Proxy.newProxyInstance(Collection.class.getClassLoader(), new Class[]{Collection.class}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
                    System.out.println("----广告开始----");
    
                    Object returnVal = method.invoke(target, args);
    
                    System.out.println("----广告结束----");
    
                    return returnVal;
    
    
                }
            });
    
            collection.add("aad");
            collection.add("aad");
            collection.add("aad");
            collection.add("aad");
            System.out.println(collection.size());
        }
    
    }
    

    当然,我们希望的是调用的东西是框架完成以后用户(配置)输入的,因此,我们需要再提供接口:

    public interface Advice {
    
        void before(Object proxy, Method method, Object[] args);
    
        void after(Object proxy, Method method, Object[] args);
    }
    

    如上所示,为了方便,接口只提供两个简单的方法,分别在方法执行前后执行。

    然后,我们也把获取代理对象的方法封装一下,用户只需要传入接口的实现类即可。

    public class ProxyTest1 {
    
        //被代理的对象
        private static ArrayList<String> target = new ArrayList<>();
    
        public static void main(String[] args) {
    
            //一步到位,获取代理类并且生成对象
            Collection collection = (Collection) getProxyInstance(Collection.class, new Advice() {
                @Override
                public void before(Object proxy, Method method, Object[] args) {
                    System.out.println(method.getName() + "开始执行");
                }
    
                @Override
                public void after(Object proxy, Method method, Object[] args) {
                    System.out.println(method.getName() + "结束执行");
                }
            });
    
            collection.add("aad");
            collection.size();
        }
    
        private static Object getProxyInstance(Class<?> clazz, Advice advice) {
            return Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
                    advice.before(proxy, method, args);
                    Object returnVal = method.invoke(target, args);
                    advice.after(proxy, method, args);
    
                    return returnVal;   
                }
            });
        }
    
    }
    

    注意:接口的方法都会交给InvocationHandler分发处理,但是由Object继承过来的方法只有toString、equals、hashCode才会交给InvocationHandler处理。

    String、StringBuilder与StringBuffer的区别

    1. 在执行速度方面的比较:StringBuilder > StringBuffer

    2. StringBuffer与StringBuilder,他们是字符串变量,是可改变的对象,每当我们用它们对字符串做操作时,实际上是在一个对象上操作的,不像String一样创建一些对象进行操作,所以速度就快了。

    3. StringBuilder:线程非安全的
        StringBuffer:线程安全的

      当我们在字符串缓冲去被多个线程使用是,JVM不能保证StringBuilder的操作是安全的,虽然他的速度最快,但是可以保证StringBuffer是可以正确操作的。当然大多数情况下就是我们是在单线程下进行的操作,所以大多数情况下是建议用StringBuilder而不用StringBuffer的,就是速度的原因。

    对于三者使用的总结:

    1. 如果要操作少量的数据用 = String
    2. 单线程操作字符串缓冲区 下操作大量数据 = StringBuilder
    3. 多线程操作字符串缓冲区 下操作大量数据 = StringBuffer

    Overload(重载)与Override(重写)的区别

    1. 重载是指不同的函数使用相同的函数名,但是函数的参数个数或类型不同。调用的时候根据函数的参数来区别不同的函数。

    2. 覆盖(也叫重写)是指在派生类中重新对基类中的虚函数(注意是虚函数)重新实现。即函数名和参数都一样,只是函数的实现体不一样。

    3. 隐藏是指派生类中的函数把基类中相同名字的函数屏蔽掉了。

    4. 方法的重写(Overriding)和重载(Overloading)是Java多态性的不同表现。 重写(Overriding)是父类与子类之间多态性的一种表现,而重载(Overloading)是一个类中多态性的一种表现。

    override(重写)
    
        1. 方法名、参数、返回值相同。
        2. 子类方法不能缩小父类方法的访问权限。   
        3. 子类方法不能抛出比父类方法更多的异常(但子类方法可以不抛出异常)。    
        4. 存在于父类和子类之间。
        5. 方法被定义为final不能被重写。
    
    overload(重载)
    
        1. 参数类型、个数、顺序至少有一个不相同。 
        2. 不能重载只有返回值不同的方法名。
        3. 存在于父类和子类、同类中。
    

    ==、hashCode与equals的区别:

    1. 基本数据类型,也称原始数据类型。byte,short,char,int,long,float,double,boolean他们之间的比较,应用双等号(==),比较的是他们的值。
    2. 复合数据类型(类)复合数据类型之间进行equals比较,在没有覆写equals方法的情况下,他们之间的比较还是基于他们在内存中的存放位置的地址值的,因为Object的equals方法也是用双等号(==)进行比较的,所以比较后的结果跟双等号(==)的结果相同。
    3. 将对象放入到集合中时,首先判断要放入对象的hashcode值与集合中的任意一个元素的hashcode值是否相等,如果不相等直接将该对象放入集合中。如果hashcode值相等,然后再通过equals方法判断要放入对象与集合中的任意一个对象是否相等,如果equals判断不相等,直接将该元素放入到集合中,否则不放入。

    规则:

    1. 如果两个对象根据equals()方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。
    2. 如果两个对象根据equals()方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生相同的整数结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表的性能。

    HashMap、HashTable的区别

    1. HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。
    2. HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。
    3. 两者使用的迭代方式不一样。

    sychronized意味着在一次仅有一个线程能够更改Hashtable。就是说任何线程要更新Hashtable时要首先获得同步锁,其它线程要等到同步锁被释放之后才能再次获得同步锁更新Hashtable。

    HashMap可以通过下面的语句进行同步:
    Map m = Collections.synchronizeMap(hashMap);

    总结:

    Hashtable和HashMap有几个主要的不同:线程安全以及速度。仅在你需要完全的线程安全的时候使用Hashtable,而如果你使用Java 5或以上的话,请使用ConcurrentHashMap吧。

    ArrayList、Vector的区别

    1. Vector是线程安全的,也就是说是它的方法之间是线程同步的,而ArrayList是线程序不安全的。
    2. 数据增长:ArrayList与Vector都有一个初始的容量大小,当存储进它们里面的元素的个数超过了容量时,就需要增加ArrayList与Vector的存储空间,每次要增加存储空间时,不是只增加一个存储单元,而是增加多个存储单元,每次增加的存储单元的个数在内存空间利用与程序效率之间要取得一定的平衡。Vector默认增长为原来两倍,而ArrayList的增长策略在文档中没有明确规定(从源代码看到的是增长为原来的1.5倍)。ArrayList与Vector都可以设置初始的空间大小,Vector还可以设置增长的空间大小,而ArrayList没有提供设置增长空间的方法。

    Java中创建对象的四种方法 收藏Java中创建对象的四种方式

    1. 用new语句创建对象,这是最常见的创建对象的方法。
    2. 运用反射手段,调用java.lang.Class或者java.lang.reflect.Constructor类的newInstance()实例方法。
    3. 调用对象的clone()方法。
    4. 运用反序列化手段,搜索调用java.io.ObjectInputStream对象的 readObject()方法。

    说说静态变量、静态代码块加载的过程和时机?

    回答:当类加载器将类加载到JVM中的时候就会创建静态变量,静态变量加载的时候就会分配内存空间。静态代码块的代码只会在类第一次初始化,也就是第一次被使用的时候执行一次。

    MVC、MVVP、MVP设计模式

    1. MVC:MVC的耦合性还是比较高,View可以直接访问Model
    2. MVP:Model、View、Presenter,View不能直接访问Model
    3. MVVM:View和Model的双向绑定,类似于Data-Binding

    JAVA容器的架构图

    如果觉得我的文字对你有所帮助的话,欢迎关注我的公众号:

    公众号:Android开发进阶

    我的群欢迎大家进来探讨各种技术与非技术的话题,有兴趣的朋友们加我私人微信huannan88,我拉你进群交(♂)流(♀)

    相关文章

      网友评论

      本文标题:JAVA面试-基础加强与巩固:反射、注解、泛型等

      本文链接:https://www.haomeiwen.com/subject/qhcgettx.html