• 注解


    注解、类加载器、动态代理

    1.1      注解

    1.1.1 注解概念和作用

    1. 什么是注解

    Annotation注解是类的组成部分,给类携带一些额外的信息,是一种代码级别的说明,是JDK1.5之后的新特性

    注释是给开发人员阅读的,注解是给程序提供相应信息的

    1. 注解的作用

    2.1编译检查:通过代码里标识注解,让编译器能够实现基本的编译检查。例如:@Override

    2.2编写文档:通过代码里标识注解,辅助生成帮助文档对应的内容

    2.3代码分析:通过代码里标识注解,对代码进行分析,从而达到取代xml目的

    1.1.2 内置注解的使用

      1. 三个基本的Annotation:

        @Override: 限定重写父类方法, 该注解只能用于方法

        @Deprecated: 用于表示某个程序元素(类, 方法等)已过时

        @SuppressWarnings: 抑制编译器警告

    1)        rawtypes 忽略类型安全

    2)        unused 忽略不使用

    3)        deprecation 忽略过时

    4)        unchecked 忽略安全检查

    5)        null 忽略空指针

    6)        all 忽略所有

    案例代码:

    public class Demo01 {

     

        @SuppressWarnings({ "rawtypes", "unused", "deprecation", "null", "unchecked"})

        public static void main(String[] args) {

           Student s = new Student();

           s.sleep();

          

           List list = new ArrayList<>();

           List list2 = new ArrayList<>();

           list.add("abc");

          

           new Thread().stop();

          

           String str = null;

           str.length();

        }

     

    }

     

    class Person {

        public void sleep() {

           System.out.println("人睡觉");

        }

    }

     

    class Student extends Person {

        @Override

        public void sleep() {

           System.out.println("学生学习后睡觉");

        }

       

        @Deprecated

        public void playLol() {

           System.out.println("完LOL");

        }

    }

    1.2      自定义注解

    1.2.1 自定义注解的语法

    修饰符 @interface 注解名 {

    }

    如:定义一个名为Student的注解

    public @interface StudentAnno {

    }

    1.2.2 注解的使用

    @注解名

    如:

    @StudentAnno

    public class Demo02 {

    }

    1.2.3 3.注解的属性

    1) 注解属性的作用:可以给每个注解加上多个不同的属性,用户使用注解的时候,可以传递参数给属性,让注解的功能更加强大。

    2) 属性声明方式: 属性类型 属性名();

    public @interface StudentAnno {

        // 给注解添加属性

        String name(); //字符串

        int age(); //整型

        String[] hobby(); //数组类型

    }

       注:如果定义了属性,那么在使用注解的时候,就一定要给属性赋值

    @StudentAnno(name="悟空", age=500, hobby={"吃桃子", "打妖怪"})

    public class Demo02 {

    }

    3) 属性默认值:

    String name() default "默认值";

        如果属性有默认值,则使用注解的时候,这个属性就可以不赋值。也可以重新赋值,覆盖原有的默认值。

    如定义注解:

    public @interface StudentAnno {

        // 3.给注解添加属性

        String name(); // 字符串

        int age() default 18; // 整型

        String[] hobby(); // 数组类型

    }

    使用注解时不赋值,使用默认值:

    @StudentAnno(name="悟空", hobby={"吃桃子", "打妖怪"})

    public class Demo02 {

     

    }

    4) 特殊属性名value

        如果注解中只有一个名称value的属性,那么使用注解时可以省略value=部分,只写属性值即可。无论这个value是单个元素还是数组,都可以省略。但如果还有其它属性需要赋值,则调用时value名字不能省略。

    public @interface EmoplyeeAnno {

        // 注解只有一个属性,且属性名是value,给value属性赋值可以写成  @EmoplyeeAnno("张三")

        String value();

    }

    使用:

    @EmoplyeeAnno("abc")

    //  @EmoplyeeAnno({"abc", "cba"})

    public static void main(String[] args) {

     

    }

    1.2.4 自定义注解练习

    1) 注解名为BookAnno

    2) 包含属性:String value(); //书名

    3) 包含属性:double price(); //价格,默认值为 100

    4) 包含属性:String[] authors(); //多位作者

    案例代码:

    public @interface BookAnno {

        String value();   // 书名

        double price() default 100; // 价格

        String[] authors();  // 作者

    }

    使用注解:

    @BookAnno(value="西游记", price=20, authors={"吴承恩", "金正恩"})

    public class Demo03 {

     

    }

    1.3      元注解

    元注解概念:在自定义注解时使用的注解,给自定义注解添加约束,称为元注解。查看@Override的源码,即可看到元注解,任何一个注解都有元注解

    1.3.1 @Target元注解

    @Target作用:指明此注解用在哪个位置,如果不写默认是全部位置

    @Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR, ElementType.LOCAL_VARIABLE})

    ElementType.TYPE: 用在类和接口上

    ElementType.FIELD:用在成员变量上

    ElementType.METHOD: 用在方法上

    ElementType.PARAMETER:用在参数上

    ElementType.CONSTRUCTOR:用在构造方法上

    ElementType.LOCAL_VARIABLE:用在局部变量

    案例代码:

    @Target(ElementType.TYPE)

    public @interface BookAnno {

        String value();   // 书名

        double price() default 100; // 价格

        String[] authors();  // 作者

    }

    自定义注解使用:

    @BookAnno(value="西游记", price=20, authors={"吴承恩", "金正恩"})

    public class Demo04 {

        // @Target(ElementType.TYPE) 明确只能用在类上面

        // @BookAnno(value="西游记", price=20, authors={"吴承恩", "金正恩"})

        public static void main(String[] args) {

        }

    }

    1.3.2 @Retention元注解

    @Retention功能:限制自定义注解的作用范围

    枚举值

    作用

    RetentionPolicy.SOURCE

    注解只存在于Java源代码中,编译成字节码文件以后删除。

    RetentionPolicy.CLASS

    注解存在于Java源代码、编译以后的字节码文件中,运行的时候内存中没有,默认值。

    RetentionPolicy.RUNTIME

    注解存在于Java源代码中、编译以后的字节码文件中、运行时的内存中,程序可以通过反射获取该注解。

    如:Override就限定只能用在源代码上:

    @Target(ElementType.METHOD)

    @Retention(RetentionPolicy.SOURCE)

    public @interface Override {

    }

    注意:如果要通过反射拿到注解,这个自定义的注解必须设置为: RetentionPolicy.RUNTIME

    1.4      解析注解

    AnnotatedElement是一个接口,只要实现这个接口的类都可以获取注解.

    Class,Constructor,Field,Method都实现这个接口

    1.4.1   AnnotatedElement接口中的方法:

    <T extends Annotation> T

    getAnnotation(Class<T> annotationClass)
              通过注解类名得到注解

     Annotation[]

    getAnnotations()
              返回此元素上存在的所有注解

     Annotation[]

    getDeclaredAnnotations()
              返回此元素上的所有注解

     boolean

    isAnnotationPresent(Class<? Extends Annotation> annotationClass)
              如果指定类型的注释存在于此元素上,则返回 true,否则返回 false。

    1.4.2 获得注解

    public final class Class<T> implements AnnotatedElement

    得到注解类对象的原则:这个注解在哪个成员上,就通过哪个对象来得到它的注解。

    如:注解用在类上,就通过类对象得到它的注解。注解用在方法上,就通过方法对象得到它的注解

    @BookAnno(value="西游记", price=20, authors={"吴承恩", "金正恩"})

    public class Demo04 {

        public static void main(String[] args) throws Exception {

           // 获取类上面的注解

           boolean b = Demo04.class.isAnnotationPresent(BookAnno.class);

           if (b) {

               BookAnno anno = Demo04.class.getAnnotation(BookAnno.class);

              

               System.out.println(anno.value());

               System.out.println(anno.price());

               System.out.println(Arrays.toString(anno.authors()));

           } else {

               System.out.println("类上面没有对应注解");

           }

          

           System.out.println("------------------");

           // 获取方法上面的注解

           Method method = Demo04.class.getMethod("test");

           boolean b2 = method.isAnnotationPresent(BookAnno.class);

           if (b2) {

               BookAnno anno = method.getAnnotation(BookAnno.class);

               System.out.println(anno.value());

               System.out.println(anno.price());

               System.out.println(Arrays.toString(anno.authors()));

           } else {

               System.out.println("方法上面没有对应注解");

           }

        }

       

        @BookAnno(value="水浒传", price=10, authors={"施耐庵", "施主"})

        public static void test() {

        }

    }

    1.1     注解案例-模拟@Test注解

    案例需求:模拟系统自带的@Test注解,自动运行带@Test注解的方法

      1) 模拟JUnit测试的注释@Test,首先需要编写自定义注解@MyTest,并添加元注解,保证自定义注解只能修饰方法,且在运行时可以获得。

      2) 其次编写目标类,然后给目标方法使用@MyTest注解,编写三个方法,其中两个加上@MyTest注解。

      3) 最后编写调用类,使用main方法调用目标类,模拟JUnit的运行,只要有@MyTest注释的方法都会运行。

    案例实现:

    1) 步骤1:编写自定义注解类@MyTest

    @Target(ElementType.METHOD)

    @Retention(RetentionPolicy.RUNTIME)

    public @interface MyTest {

     

    }

    2) 步骤2:编写目标类MyTestDemo

    public class MyTestDemo {

        @MyTest

        public void test1(){

          System.out.println("test1执行了...");

        }

       

        @MyTest

        public void test2(){

          System.out.println("test2执行了...");

        }

       

        public void test3(){

          System.out.println("test3执行了...");

        }

    }

    3) 步骤3:编写测试方法

        3.1) 得到使用注解的类对象

        3.2) 创建类的实例

        3.3) 得到类中所有公有的方法

    3.4) 遍历所有的方法,如果方法上有注解,则运行此方法

    public class Demo05 {

     

        public static void main(String[] args) throws Exception {

           // 1.1 反射:获得类的字节码对象.Class

           Class clazz = MyTestDemo.class;

           // 1.2 获得实例对象

           Object obj = clazz.newInstance();

     

           // 2 获得目标类所有的方法

           Method[] allMethod = clazz.getMethods();

           // 3 遍历所有的方法

           for (Method method : allMethod) {

               // 3.1 判断方法是否有MyTest注解

               if (method.isAnnotationPresent(MyTest.class)) {

                  // 4 如果有注解运行指定的类

                  method.invoke(obj);

               }

           }

        }

    }

    第2章     类加载器

    2.1      类加载器的作用

    类加载器是负责将class字节码文件,从硬盘加载到内存中,并且生成Class类对象

     

    类加载器的共同父类:java.lang.ClassLoader

    2.2      三种类加载器

    1) 引导类加载器:

        BootstrapClassLoader 最底层的加载器,由C和C++编写,不是Java中的类。

        作用:加载JRE最基础的Java类,如:rt.jar

            

    2) 扩展类加载器:

        ExtClassLoader 由Java程序编写,是一个Java类。作用:加载JRE中的扩展类

            

    3) 应用类加载器:

        AppClassLoader 由Java程序编写,是一个Java内部类。作用:加载CLASSPATH指定的jar(包括第三方的库)和自己编写的类。

      

    4. 如何得到类加载器:

        类名.class.getClassLoader()

    2.3      类加载器的示例

      1. 示例:创建三个测试类,每个测试类写2个测试方法。

    l  预备知识:

        1) 使用System.getProperties()查看所有JVM加载的系统环境配置信息

          通过System.getProperty(常量名),指定常量的名字,得到一个常量的值。

    public class Demo06 {

     

    public static void main(String[] args) {

         Properties properties = System.getProperties();

         Set<String> names = properties.stringPropertyNames();

         for (String name : names) {

            System.out.println(name + "=" + System.getProperty(name));

         }

    }

    }

        2) 输出每种类加载器加载的内容

          其实是一些加载路径,使用分号分隔,我们可以使用split()方法拆分以后输出。

    public class Demo06 {

    public static void main(String[] args) {

         /*Properties properties = System.getProperties();

         Set<String> names = properties.stringPropertyNames();

         for (String name : names) {

            System.out.println(name + "=" + System.getProperty(name));

         }*/

        

         String boots = System.getProperty("sun.boot.class.path");

         String[] strings = boots.split(";");

         for (String string : strings) {

            System.out.println(string);

         }

        

         System.out.println("-------------------");

        

         String exts = System.getProperty("java.ext.dirs");

         String[] strings2 = exts.split(";");

         for (String string : strings2) {

            System.out.println(string);

         }

    }

    }

        3) 输出每种类加载器的类型,使用方法getClassLoader()。

      2. 引导类加载器

        ● 常量:sun.boot.class.path

    1) 代码:使用String类,因为String类在java.lang包中,由引导类加载器加载。

    // 测试引导类加载器

    @Test

    public void test2() {

       System.out.println("引导类加载器加载路径");

       String boots = System.getProperty("sun.boot.class.path");

       String[] strings = boots.split(";");

       for (String string : strings) {

           System.out.println(string);

       }

     

       ClassLoader loader = String.class.getClassLoader();

       System.out.println("引导类加载器 " + loader);

    }

        2) 加载的内容:

    C:developJavajdk1.7.0_72jrelib esources.jar

    C:developJavajdk1.7.0_72jrelib t.jar

    C:developJavajdk1.7.0_72jrelibsunrsasign.jar

    C:developJavajdk1.7.0_72jrelibjsse.jar

    C:developJavajdk1.7.0_72jrelibjce.jar

    C:developJavajdk1.7.0_72jrelibcharsets.jar

    C:developJavajdk1.7.0_72jrelibjfr.jar

    C:developJavajdk1.7.0_72jreclasses

        3) 加载器的类型:

          因为引导类加载器不是类,所以返回null。

     

      3. 扩展类加载器

        ● 常量:java.ext.dirs

        注:如果要使用lib/ext包中的类,要在eclipse中要进行如下设置。

        在“Project Properties-->Java Build Path”中的指定JRE包的访问规则,Edit规则。

        Accessible,指定为sun/**,指定可以在eclipse中访问sun开头的包。

     

       

    1) 代码:使用任何一个在ext包中的类

    // 测试扩展类加载器

    @Test

    public void test3() {

       System.out.println("扩展类加载器加载路径");

       String boots = System.getProperty("java.ext.dirs");

       String[] strings = boots.split(";");

       for (String string : strings) {

           System.out.println(string);

       }

     

       ClassLoader loader = DNSNameService.class.getClassLoader();

       System.out.println("扩展类加载器 " + loader);

    }

    2) 加载的内容:

    C:developJavajdk1.7.0_72jrelibext

    C:WindowsSunJavalibext

        3) 加载器的类型:

    sun.misc.Launcher$ExtClassLoader@153d05b

     

      4. 应用类加载器

        ● 常量:java.class.path

       

        1) 代码:自己编写的类,由应用类加载器加载

    // 测试应用类加载器

    @Test

    public void test4() {

       System.out.println("应用类加载器加载路径");

       String boots = System.getProperty("java.class.path");

       String[] strings = boots.split(";");

       for (String string : strings) {

           System.out.println(string);

       }

     

       ClassLoader loader = Demo06.class.getClassLoader();

       System.out.println("应用类加载器 " + loader);

    }

        2) 加载的内容:

    C:Userszhangpingworkin

    C:developeclipsepluginsorg.junit_4.12.0.v201504281640junit.jar

    C:developeclipsepluginsorg.hamcrest.core_1.3.0.v201303031735.jar

    /C:/develop/eclipse/configuration/org.eclipse.osgi/383/0/.cp/

    /C:/develop/eclipse/configuration/org.eclipse.osgi/382/0/.cp/

    3) 加载器的类型:

    sun.misc.Launcher$AppClassLoader@7692ed85

       

    1.2     三个类加载器的关系

      1. 示例:得到当前类的类加载器,并输出每个类加载器的父类加载器。

    // 测试三个类加载器关系

    @Test

    public void test5() {

        ClassLoader loader = Demo06.class.getClassLoader();

        // sun.misc.Launcher$AppClassLoader@a53948

        System.out.println("应用类加载器" + loader);

     

        ClassLoader parent = loader.getParent();

        // sun.misc.Launcher$ExtClassLoader@153d05b

        System.out.println("上一层类加载器" + parent);

     

        ClassLoader parent2 = parent.getParent();

        // null表示引导类加载器

        System.out.println("上一层的上一层类加载器" + parent2);

    }

       

    ● 输出结果

    应用类加载器 sun.misc.Launcher$AppClassLoader@a53948

    上一层类加载器sun.misc.Launcher$ExtClassLoader@153d05b

    上一层的上一层类加载器null

          

      2. 结论:

        1) 应用类加载器AppClassLoader的父加载器是扩展类加载器ExtClassLoader

    2) 扩展类加载器ExtClassLoader的父加载器是引导类加载器BootstrapClassLoader

    3) 注:不存在父类与子类的关系,它们都是Launcher的内部类

     

    1.3     类加载器中的方法

      1. URL getResource() 查找具有给定名称的资源,返回URL。

             一般使用URL类中的getPath()方法,得到具体的路径。

       

      2. InputStream getResourceAsStream(String name) 返回读取指定资源的输入流

       

      3. 示例:在src目录下创建一个bean.xml文件,使用类加载器的方法

        1) 得到文件的路径

        2) 得到文件的输入流

    public class Demo07 {

        public static void main(String[] args) throws Exception {

           ClassLoader loader = Demo07.class.getClassLoader();

           // 得到类路径下指定的文件的路径

           URL url = loader.getResource("Info.txt");

           System.out.println(url.getPath());

          

           // 得到类路径下指定的文件,并且转成相应的输入流

           InputStream is = loader.getResourceAsStream("Info.txt");

           byte[] b = new byte[1024];

           int len = 0;

           while ((len = is.read(b)) != -1) {

               System.out.println(new String(b, 0, len));

           }

        }

    }

    第3章     代理模式

    3.1      代理模式的分类

      代理模式分成静态代理和动态代理 

    3.2      代理模式涉及到的对象

     

    1. 抽象对象:是真实对象与代理对象的共同接口

    2. 真实对象:对象本身,如:明星

           3. 代理对象:相当于真实对象的一个代理对象如:经纪人

    1) 拥有真实对象的成员变量

    2) 通过构造方法传入真实对象

    3) 可以改写真实对象的方法或对真实对象的方法进行拦截

      4. 调用者:使用真实对象的消费者,如:明星的粉丝

    3.3      静态代理的实现

    1. 抽象对象:明星

    public interface Star {

        // 唱歌方法

        public abstract void sing();

       

        // 跳舞方法

        public abstract void dance();

    }

    2. 真实对象:

    public class BabyStrong implements Star {

     

        @Override

        public void sing() {

           System.out.println("王宝强: 唱天下无贼...");

        }

     

        @Override

        public void dance() {

           System.out.println("王宝强: 跳一个人的武林...");

        }

    }

    3. 代理对象:

    public class StarProxy implements Star {

        private BabyStrong bs;

       

        public StarProxy(BabyStrong bs) {

           this.bs = bs;

        }

     

        @Override

        public void sing() {

           System.out.println("经纪人: 弹出场费");

           bs.sing();

           System.out.println("经纪人: 收取出场费,和宝强分成");

        }

     

        @Override

        public void dance() {

           System.out.println("经纪人: 出场费不够,谈崩了...没有下文");

        }

    }

    4. 调用者:

    public class Fans {

        public static void main(String[] args) {

           // 创建真实对象

           BabyStrong bs = new BabyStrong();

           // 创建代理对象

           StarProxy sp = new StarProxy(bs);

           // 调用代理对象的方法

           sp.sing();

           sp.dance();

        }

    }

    3.4      静态代理模式的特点

    1. 优点:

    在不修改现有类的前提下,对现有类的功能进行扩展和修改

    可以拦截真实对象的方法

     

    2. 缺点:

    一个真实对象必须对应一个代理对象,如果大量使用会导致类的急剧膨胀。

    如果抽象对象中方法很多,则代理对象也要编写大量的代码。

    3.5      动态代理模式

      1. 特点:

        1) 动态生成代理对象,不用手动编写代理对象

        2) 不需要编写目标对象中所有同名的方法

       

      2. 动态代理类相应的API:

    1. Proxy类:

    static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)

    ²  newProxyInstance()作用:生成动态代理对象

     

    ²  参数说明:

    loader参数:真实对象的类加载器

    interfaces参数:真实对象所有实现的接口数组

    h参数: 具体的代理操作,InvocationHandler是一个接口,需要传入一个实现了此接口的实现类。

    Object返回值:生成的代理对象

    2.InvocationHandler接口:

    Object invoke(Object proxy, Method method, Object[] args)

    ²  invoke作用:每次调用代理对象的方法,都会走到这个方法来.在这个方法中实现对真实方法的增强或拦截

     

    ²  参数说明:

    proxy:即方法newProxyInstance()方法返回的代理对象,该对象一般不要在invoke方法中使用,容易出现递归调用。

    method: 真实对象的方法对象,会进行多次调用,每次调用method对象都不同。

    args:代理对象调用方法时传递的参数

    返回值:是真实对象方法的返回值

    3.6      动态代理模式的开发步骤

    1. 定义接口
    2. 定义真实类
    3. 直接创建真实对象
    4. 通过Proxy类,创建代理对象
    5. 调用代理方法,其实是调用InvocationHandler接口中的invoke()方法

    3.7      动态代理模式代码

    Work接口:

    public interface Work {

        // 搬砖方法

        public abstract void moveBricks(int count);

       

        // 敲代码方法

        public abstract void coding();

    }

    LaoWang真实对象类:

    public class LaoWang implements Work {

     

        @Override

        public void moveBricks(int count) {

           System.out.println("王宝强: 一次搬" + count + "块转");

        }

     

        @Override

        public void coding() {

           System.out.println("王宝强: 疯狂的敲代码");

        }

    }

    Boss调用类:

    public class Boss {

     

        public static void main(String[] args) {

           // 创建真实对象

           final LaoWang laoW = new LaoWang();

          

           // 创建动态代理,这个代理实现Work接口

           // loader:真实对象的类加载器 LaoWang.class

           // interfaces: 真实对象的类所有实现的接口数组Work

           // h: 具体怎么代理

           Work obj = (Work)Proxy.newProxyInstance(

                  // 真实对象的类加载器 LaoWang.class

                  LaoWang.class.getClassLoader(),

                  // 真实对象的类所有实现的接口数组

    //            new Class[]{Work.class}

                  LaoWang.class.getInterfaces(),

                  // 具体的代理操作

                  new InvocationHandler() {

                      @Override

                      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                         // 代理对象

    //                   System.out.println(proxy);

                         // 被代理对象调用的方法

    //                   System.out.println(method.getName());

                         // 被调用方法的参数

    //                   System.out.println(Arrays.toString(args));

                        

                         System.out.println("先执行代理,代理类再调用真实类的方法: " + method.getName());

                         method.invoke(laoW, args);

                         return null;

                      }

                  });

          

           // 使用代理对象调用方法

           obj.moveBricks(10);

           obj.coding();

        }

    }

    3.8      动态代理练习

    需求:

    有一个ArrayList<String> arrayList = new ArrayList<>();使用动态代理增强ArrayList的add方法,每个add到ArrayList中的元素都添加”代理 :”字符串.如arrayList.add(“张三”);实际添加到ArrayList中的内容为”代理 :张三”

    案例代码:

    public class Demo10 {

     

        @SuppressWarnings("unchecked")

        public static void main(String[] args) {

           //  真实对象

           final ArrayList<String> arrayList = new ArrayList<>();

          

           // 创建动态代理

           List<String> listProxy = (List<String>)Proxy.newProxyInstance(

                  arrayList.getClass().getClassLoader(),

                  arrayList.getClass().getInterfaces(),

                  new InvocationHandler() {

                     

                      @Override

                      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                        

                         if (method.getName().equals("add")) {

                             args[0] = "代理 : " + args[0];

                         }

                         Object result = method.invoke(arrayList, args);

                         return result;

                      }

                  });

          

           listProxy.add("张三");

           listProxy.add("李四");

           listProxy.add("王五");

          

           for (String obj : arrayList) {

               System.out.println(obj);

           }

        }

     

    }

  • 相关阅读:
    git初学【常用命令、上传项目到码云或从码云拉取、克隆项目】
    dedecms自学
    sublime3使用笔记
    路由功能
    bootstrap模态框篇【遇到的问题】
    justgage.js的使用
    fullpage.js使用方法
    js&jq遇到的问题(不断更新中)
    图灵完备——停机问题
    中断
  • 原文地址:https://www.cnblogs.com/fengyangcai/p/12888477.html
Copyright © 2020-2023  润新知