目录
面向对象程序设计
使用命令行工具简单的编译源码
数据类型
StringBuilder
数组
对象与类
理解方法调用
继承
代理
异常
断言
日志
面向对象程序设计
- 面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分
- 从根本上说,只要对象能够满足要求,就不必关心其功能的具体实现过程
- 在OOP中,不必关心对象的具体实现,只要能满足用户的需求即可
- 传统的结构化程序设计通过设计一系列的过程(即算法)来求解问题
- 一旦确定了这些过程,就要开始考虑存储数据的方式
- 即算法是第一位,数据结构是第二位
- 这就明确地表述了程序员的工作方式
- 首先要确定如何操作数据,然后再决定如何组织数据,以便于数据操作
- 而OOP却调换了这个次序,将数据放在第一位,然后再考虑操作数据的算法
- 对于一些规律较小的问题,将其分解为过程的开发方式比较理想
- 而面向对象更加适用于解决规模较大的问题
- 要想实现一个简单的Web浏览器可能需要大约2000个过程,这些过程可能需要对一组全局数据进行操作
- 采用面向对象的设计风格,可能只需要大约100个类,每个类平均包含20个方法
- 每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务
- 对象可以复用,通过继承机制还可以定制
- 因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目
- 面向对象三大特征,继承、封装、多态
- 借助Java中的Collection理解
- 首先是继承,ArrayList与LinkedList均实现了list接口,因此具有了list接口的方法(如add、indexOf),虽然这两个类实现有所不同,但是因为实现自同一个接口,所以实现的功能是一致的
- 将相同的部分向上抽取,然后由不同的子类去实现,这种设计的好处还要与多态联系起来
- 然后是多态
- 在需要使用ArrayList时,我们倾向于将对象引用声明为list
- 这便是多态
- 当以后需要将ArrayList改为其他集合,只要这个集合实现了list,那么便无需大范围修改代码
- 最后是封装,ArrayList中的size成员变量,在实际使用时无需设置,只需要查询其值
- 因此,Java开发人员在设计这个类时,用private修饰size,并且只提供了访问器方法,避免size遭到修改
- 我所理解的面向对象,无非就是一套程序设计规范,按照这个规范,可以设计出扩展性、安全性都有不错表现的程序
使用命令行工具简单的编译源码
- 创建如下源代码Welcome.java:
- 执行下面的命令可以编译和运行;并产生结果
数据类型
- Java中有两大数据类型:内置数据类型和引用数据类型
- 内置数据类型:
- 注意:
- 声明变量可以;但进行使用时,必须初始化之后才能使用
- Java中不区分变量的声明和定义
- 自动类型转换:整型、实型(常量)、字符型数据可以混合运算;运算中,不同类型的数据先转化为同一类型,然后进行运算
- 低 —> 高
- byte,short,char —> int —> long—> float —> double
- 不能对boolean类型进行类型转换
- 不能把对象类型转换成不相关类的对象
- 在把容量大的类型转换为容量小的类型时必须使用强制类型转换
- 转换过程中可能导致溢出或损失精度
- 引用数据类型:
- 在Java中,引用类型的变量非常类似于C/C++中的指针
- 引用类型指向一个对象,指向对象的变量是引用变量;对象和数组都是引用数据类型
- 所有引用类型的默认值都是null
- 一个引用变量可以用来引用任何与之兼容的类型
- 注意:
- 对象只有在实例化之后才能被使用,而实例化对象的关键字就是new
- 对象的实例化过程如下:
- 常量:Java中利用关键字final指示常量
StringBuilder
- JDK5.0中引入StringBuilder类
- 非线程安全,实现高效的字符串处理;常用方法如下:
数组
- Java中的数组实际上的内存分配是在栈中(int[] 和new int[] 相同)
- 因此多维数组都是指针数组
- 每个元素都是指针,存放的是地址值
- 因此不必长度相同
- 使用Arrays.copyOf()和Arrays.sort()进行深拷贝和排序
- 注意Java中[]运算符被预定义为检查数组边界,没有指针预算;不能通过+1操纵得到数组的下一个元素
对象与类
- 更改器是会改变原来的数据;比如set方法
- 访问器只是会查看数据;比如get方法
- 而构造器就是创建
- 注意类中的更改器方法一般都没有更改原来的数据,而是产生了一个新的数据
- 简单来说就是写时复制
- Java多个源文件的使用:
- 当使用Java编译.java文件时,如果程序使用了xxx类,则javac会自动查找名为xxx.class的文件,没有则查找xxx.java然后对其进行编译
- 如果xxx.java版本比xxx.class版本更新,则自动地重新编译这个文件
- Java编译器内置了make功能
- 注意:
- Java构造器的工作方式与C++相同
- 但是所有的Java对象都是在堆中构造的,因此必须使用new操作符
- 避免在构造函数中使用与全局变量同名的局部变量
- Java和C++都使用this作为隐式参数进行函数方法和变量的调用和声明
- Java中的所有方法都必须在类的内部定义
- 注意不要编写返回引用可变对象的访问器方法
- 因为Java中的对象都是指针,返回值拷贝也是指针值的拷贝
- 因此会存在可变对象,破坏了类的封装性
- 可以使用clone()方法,对对象进行深拷贝
- 使用final,相当于C++中的const,定义必须初始化
- Java中的静态方法与C++中定义相同,但是是直接通过类名.方法名使用
- 而不是解引用方法使用
- Java中的所有方法都是按值进行参数传递的
- 方法参数:
- Java为了实现C++中的引用功能–改变传入对象,将所有的基本类型都做了类对象的封装
- 并通过开箱与装箱实现两者之间的快速转换
- 构造方法:
- 面向对象编程的第一步,就是要生成对象
- 前面说过,对象是单个实物的抽象
- 通常需要一个模板,表示某一类实物的共同特征,然后对象根据这个模板生成
- 所谓“类”就是对象的模板,对象就是“类”的实例
- 所谓”构造方法”,就是专门用来生成实例对象的方法,是类的一环
- new 命令的原理:
- 使用new命令时,它后面的方法依次执行下面的步骤:
- 1-创建一个空对象,作为将要返回的实例对象
- 2-将这个空对象的原型,指向构造方法的prototype属性
- 3-将这个空对象赋值给方法内部的this关键字
- 4-开始执行构造方法内部的代码(代码中this指向空对象(实例对象))
- 5-返回实例对象(或自定义对象)
- 对象构造:
- 重载:Java允许重载任何方法,而不只是构造器方法
- 默认域初始化:在构造器中没有显式地给域赋予初值,那么就会被自动地赋值为默认值
- 默认构造器:当类中没有任何构造器的时候,编译器才会提供无参构造器;存在任何一个构造器,其无参构造器都会消失
- 显示初始化:建议在定义时,显示初始化默认参数
- 构造方法输入参数命名:可以和成员变量相同,但是需要使用this指针区别两者
- 块初始化:可以使用块初始化代码,让部分代码在构造函数之前进行运行;或者使用static关键字构造相同功能的代码块;为了避免循环定义,应该将初始化代码块放在变量定义域之后
- 调用构造器的具体步骤:
- 1-所有数据域被初始化为默认值
- 2-按照在类声明中出现的次序,一次执行所有初始化语句和初始化块
- 3-构造器第一行调用了第二个构造器,则执行第二个构造器主体
- 4-执行这个构造器的主体
- 包:
- 可以使用完整的路径名进行类的导入,也可以使用import
- 避免重复的方法
- 当相同包中存在重复,需要再import一次明确相同类的来源
- 使用package xxx.xx.xx;方法将类导入到包中
- 包的路径,一般都是文件夹路径
- 因此可以直接在对应的package路径中找到对应的.class文件
- 非public类,同一个包中可以访问;包外不行
- 类路径:
- 类存储在文件系统的子目录中,类的路径必须与包名匹配
- 为了类能够被多个程序共享,需要做到以下几点:
- 1-类放到一个目录中;包树状结构的基目录;文件路径必须和包名匹配
- 2-将JAR文件放在一个目录中
- 3-设置类路径(class path);包含所有类文件的路径的集合
- 4-可以在运行时,使用-classpath参数指定类的路径;类似于C++的LIB_PATH
- 类的设计技巧:
- 保证数据私有
- 一定要对数据初始化
- 不要在类中使用过多的基本类型
- 不是所有的域都需要独立的访问器或者修改器
- 将职责过多的类进行分解
- 类名和方法名要能够体现他们的职责
- 优先使用不可变的类
- 解决默认方法冲突:
- 1-超类优先;如果超类提供了一个具体方法,同名且有相同参数类型的默认方法会被忽略
- 2-接口冲突;如果一个超类提供了一个默认方法,另外一个接口提供了一个同名且参数类型相同的方法;必须覆盖这个方法来解决冲突
- 3-继承的类优先原则:继承超类和接口冲突时,优先考虑类方法–“类优先”原则
- 对象克隆:
- 因为Java中的所有类变量都是指针
- 因此会存在浅拷贝的问题
- 虽然Object存在一个protected clone()方法
- 但是它只能针对基本数据类型;对于类类型不会进行递归的拷贝
- 因此需要每个类重载或者定义自己的clone方法
- 为了实现这个功能;提供了Cloneable接口,来重写clone方法,进行深拷贝
- Cloneable是标记接口(其方法体为空),它用来表示一个类拥有某些希望具有的特征
- 实现Cloneable接口的类被标记为可克隆的,而且其对象可以使用Object类中定义的clone()方法克隆
- 如果没有实现Cloneable类对象,调用clone()就回抛出CloneNotSupportedException异常
- 如果一个数据类型是基本类型,复制的就是它的值
- 如果一个数据域是对象,复制的就是该域的引用
- 下面通过一个类来演示Cloneable接口的用法,以及深、浅复制区别:
- 测试类代码:
- house1和house2是两个内容相同的不同对象
- Object类中的clone()方法将原始对象的每个数据域复制给目标对象
- 尽管house1 == house2为假,但是house1.whenBulit == house2.whenBuilt为真
- 这就是浅复制而不是深复制,这意味着如果数据域是对象类型,那么复制的是对象的引用,而不是他的内容
- 如果希望House对象执行深复制,将clone()方法中的代码换成被注解的部分
- 这时候house1.whenBulit == house2.whenBuilt为假
- house1和house2包含两个不同的Date对象
理解方法调用
- 假设要调用x.f(param),隐式参数x声明为类C的一个对象
- 下面是调用过程的详细描述:
- 1-编译器查看对象的声明类型和方法名
- 假设调用x.f(param),且隐式参数x声明为C类的对象
- 需要注意的是:有可能存在多个名字为f,但参数类型不一样的方法
- 例如,可能存在方法f(int)和方法f(String)
- 编译器将会列举所有C类中名为f的方法和其超类中访问属性为public且名为f的方法(超类的私有方法不可访问)
- 至此,编译器已获得所有可能被调用的候选方法
- 2-接下来,编译器将查看调用方法时提供的参数类型
- 如果在所有名为f的方法中存在一个与提供的参数类型完全匹配,就选择这个方法
- 这个过程被称为重载解析(overloading resolution)
- 由于允许类型转换,所以这个过程可能会很复杂
- 如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,就会报告一个错误
- 至此,编译器已获得需要调用的方法名字和参数
- 2.1-方法的名字和参数列表称为方法的签名
- 如果在子类中定义了一个与超类签名相同的方法,那么子类中的这个方法就覆盖了超类中的这个相同签名的方法
- 2.2-返回类型不是签名的一部分,因此,在覆盖方法时,一定要保证返回类型的兼容性
- 允许子类将覆盖方法的返回类型定义为原返回类型的子类型
- 3-如果是private方法、static方法、final方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定(static binding)
- 与此对应的是,调用方法依赖于隐式参数的实际类型,并且在运行时动态绑定;如x.f(param)
- 4-当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法
- 假设x的实际类型是D,它是C类的子类
- 如果D类定义了一个方法f(String),就直接调用它
- 否则,将在D类的超类中寻找f(String),以此类推
- 4.1-每次调用方法都要进行搜索,时间开销相当大
- 因此,虚拟机预先为每个类创建了一个方法表(method table),其中列出了所有方法的签名和实际调用的方法
- 这样一来,在真正调用方法的时候,虚拟机仅查找这个表就可以了
- 如果调用了super.f(param),编译器将对隐式参数超类的方法表进行搜索
继承
代理
异常
断言
日志
- Java中对于普通类的继承直接使用extends代替C++中的:进行继承操作
- 而且Java中为了避免菱形继承问题,使用了interface,作为抽象基类接口
- 对于extends只能单继承,interface可以使用implements来实现多个接口类的继承,符合面向对象设计原则中的面向接口编程
- 同时提供了abstract关键字修饰抽象类;相当于C++中的虚基类
- 注意:
- Java中的所有继承都是公有继承
- Java中使用super关键字区分父类方法和子类方法;方便进行方法的重载;同时使用super显示进行父类构造函数的调用
- Java中的对象变量是多态的(毕竟指针);因此可以实现向下类型的安全转变
- 但是要注意类型转变时的方法和名称关系
- 向上的类型转换一般都是不安全的要注意方法调用
- 如果是private、static、final修饰的方法或者构造器这些都是静态绑定
- 编译器可以准确地找到调用的方法
- 此外的方法
- 编译器会为每个类创建一个方法表,列出所有的方法签名和实际调用的方法
- 在运行时由虚拟机进行方法查找
- 指明类的方法和实际对应的函数代码段
- 在覆盖一个方法的时候,子类方法不能低于超类方法的可见性
- 特别是,如果超类方法是public则子类方法一定要声明为public
- 使用final关键字可以禁止类的继承和方法的覆盖
- 在进行向下的类型转换之前,都应该使用instanceof关键字检查类型是否能够被强制转换成功
- Java类型转换失败时会产生一个异常
- abstract修饰的抽象类不能够被实例化
- 但是可以创建一个具体的子类,用来指向抽象类
- public:对所有类可见,protected:对本包和所有子类可见;private:本类可见;默认:对本包可见
- Object 所有类的超类:
- Java中的所有类都是由Object类扩展而来的
- 因此可以将任何类型的变量向下转换为Object类型的变量
- Java语言规范要求equals方法具有下面的特性:
- 自反性:能够识别自身
- 对称性:对于任何引用x,对于x.equals(y)结果应该和y.equals(x)相同
- 传递性:a=c,c=b,则有a=c;
- 一致性:x,y引用对象没有发生变化,反复调用应该返回同样的结果
- 对任意非空引用x,x.equals(null)应该返回false
- 编写一个完美的equals方法的建议:
- hashCode()方法:
- 每个对象都有一个默认的散列码,其值为对象的存储地址
- 如果重新定义equals方法,就必须重新定义hashCode方法,以便将对象插入到散列表中
- 调用任意方法:
- Java中可以使用class类对应的getMethod方法查找字符串对应的方法
- 返回一个Method class类
- 然后使用Method的invoke方法就可以执行对应的函数了
- 继承的设计技巧:
- 将公共操作和域放在超类
- 不要使用受保护的域:protected域对于子类无限制,很容易破坏其封装性;其应该用于那些不提供一般用途而应该在子类中重新定义的方法
- 使用继承实现”is-a”关系
- 除非所有继承的方法都有意义,否则不要使用继承
- 在覆盖方法时,不要改变预期的行为
- 使用多态,而非类型信息;对于相同类,应该统一接口
- 不要过多的使用反射:反射很脆弱,编译器很难帮助人们发现程序中的错误,只有在运行时才发现错误并导致异常
- Java代理的类型:
- 静态代理:在编译时就已经确定代理类和原始类的关系,代理类通常手动编写
- 动态代理:在Java运行时动态生成代理类;这种代理利用了Java的java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口来创建代理对象,允许开发者在运行时动态地处理对于目标对象的方法调用
- 代理类:
- 代理类可以在运行时创建全新的类
- 它能帮助程序员在不修改目标对象代码的情况下增加或控制对这些对象的操作
- 代理模式在Java开发中非常有用,特别是在处理横切关注点(如日志、安全性、事务处理等)时尤为重要
- 这样的代理类能够实现指定的接口
- 尤其是,它具有下列方法:
- 1-指定接口所需要的全部方法
- 2-Object类中的全部方法
- 然而,不能在运行时定义这些方法的新代码
- 而是要提供一个调用处理器(invocation handler)
- 调用处理器是实现了InvocationHandler接口的类对象
- 在这个接口中只有一个方法:
- object invoke(Object proxy,Method method,Object[] args)
- 无论何时调用代理对象的方法,调用处理器的invoke方法都会被调用,并向其传递Method对象和原始的调用参数
- 调用处理器必须给出处理调用的方式
- 代理类的特性:
- 代理类是在程序运行过程中创建的
- 然而一旦被创建,就变成了常规类,与虚拟机中的任何其他类没有什么区别
- 所有的代理类都扩展于Proxy类
- 一个代理类只有一个实例域-调用处理器,它定义在Proxy的超类中
- 为了履行代理对象的职责,所需要的任何附加数据都必须存储在调用处理器中
- 所有的代理类都覆盖了Object类中的方法toString、equals和hashCode
- 如果所有的代理方法一样,这些方法仅仅调用了处理器的invoke
- 对于特定的类加载器和预设的一组接口来说,只能有一个代理类
- 也就是说,如果使用同一个类加载器和接口数组调用两次newProxyInstance方法的话,那么只能够得到同一个类的两个对象
- 也可以利用getProxyClass方法获得这个类
- Class proxyClass = Proxy.getProxyClass(null,interfaces);
- 可以通过调用Proxy类中的isProxyClass方法检测一个特定的Class对象是否代表一个代理类
- java.lang.reflect.InvocationHandler
- Object invoke(Object proxy,Method method , Object[] args)
- 定义了代理对象调用方法时希望执行的动作
- java.lang.reflect.Proxy
- static Class getProxyClass(ClassLoader loader, Class... interfaces)
- 返回实现指定接口的代理类
- static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler handler)
- 构造实现指定接口的代理类的一个新实例
- 所有方法都会调用给定处理器对象的invoke方法
- ClassLoader
- 类加载器
- null表示使用默认的类加载器
- Class<?>[]
- Class对象数组,每个元素都是需要实现的接口
- InvocationHandler
- 一个调用处理器
- static boolean isProxyClass(Class<?> cl)
- 如果cl是一个代理类则返回true
- 主要用途:
- 访问控制:通过代理类,可以在访问目标对象之前进行一些前置处理,如权限验证,或在访问后进行一些后置处理,如日志记录
- 延迟实例化:代理可以延迟目标对象的创建过程,直到实际需要使用该对象时才创建;这种方式通常用于资源消耗较大的对象创建,可以优化应用的启动和运行速度
- 远程对象访问:远程代理可以使得一个对象在不同的地址空间中被访问,就如同访问本地对象一样;这是分布式系统中常用的模式,例如在不同机器或应用之间进行通信
- 日志记录与监控:代理类可以自动记录所有对目标对象的访问信息,对于调试、监控和优化应用性能非常有帮助
- 异常分类
- 异常都派生于Throwable类,下层分解为:Error、Exception
- Error:Java运行时系统的内部错误和资源耗尽错误,应用程序不抛此类型
- Exception:下层分为RuntimeException、其他异常(例如:IOException)
- RuntimeException:程序错误导致的异常,例如:数组越界、null指针
- 其他异常:程序本身没问题,由于像I/O错误问题导致的属其他异常
- 非受查异常与受查异常
- 非受查异常包括:派生于Error、RuntimeExceptioin的错误或异常
- 受查异常:除非受查异常外的其他异常(此类异常需要进行异常处理(编译器将核查是否为所有的受查异常提供了异常处理器))
- 应该抛异常情况:
- 1-调用一个抛出受检查异常的方法
- 2-程序运行过程中发现错误,并利用throw语句抛出一个受查异常
- 3-程序出现错误,如数组越界
- 4-Java虚拟机和运行时库出现的内部错误
- 1、2两条是需要使用throws声明显示抛出的,或者捕获异常
- 3、4两条为非检查异常,要么不可控、要么应该避免,故不强制一定抛出
- 注意:
- 子类覆盖超类方法,抛出异常不能比超类的异常更通用,只能更特定或不抛出
- 没有throws说明符的方法不能抛出任何受查异常
- 捕获异常
- throw 和 throws
- throws,声明这个方法会抛出这种类型的异常,使其他地方调用它时知道要捕获这个异常
- throw,try/catch模块中抛出一个异常对象,表示异常已经发生,会终止程序运行(finally依然会运行)
- 捕获异常通常原则:应该捕获知道如何处理的异常,不知如何处理的继续传递
- 例外:子类覆写超类方法,而该方法没抛出异常子类必须捕获所有受查异常
- Java SE 7 及之后,单个 catch 支持捕获多个异常类型,但不存在子类关系,语法:
- 此时,e 变量为隐含的 final 的变量只能被赋值为其中之一,这种语法更简洁更高效
- 再次抛出异常与异常链:
- (1)catch子句可以抛出异常,可以起到如下作用:
- 1-出于异常识别分类,改变异常类型
- 2-受查异常转运行异常(用于不能抛出受查异常的情形)
- 3-记录异常日志
- (2)catch子句抛出新异常时,可以调用 newE.initCause( oldE ) 使得原始异常不丢失
- 注意:
- Java SE 7之后,抛出比声明的异常更广的类型时,只要try块仅有声明的异常类型,且在catch块没被改变,声明的异常是合法的
- 如果final块也可能有异常时,最佳实践代码如下:
- 这样可以解耦 try/catch 和 try/finally,使职责清晰明了
- try/finally 只负责确保异常后仍可执行 finally 的语句
- try/catch 只负责记录出现的错误
- 嵌套try时,很可能会出现外层异常覆盖了里层异常,导致原始异常丢失
- 在外层增加个里层异常是否为空的判断,如果不为空抛出里层异常否则抛出外层异常
- 例如:
- 当然,Java SE 7有自动关闭资源的特性,会比这里的处理更为简洁
- 不带资源的try语句如果try块抛出一个异常,而且close方法也抛出一个异常,这就会带来一个难题:try块抛出的异常会被close方法抛出的异常覆盖
- 带资源的try语句可以很好地处理这种情况
- 原来的异常会重新抛出,而close方法抛出的异常会“被抑制”
- try块退出时,会自动关闭资源,catch和finally子句会在关闭资源之后执行
- 当in关闭出现异常时,会被自动捕获并被抑制
- 异常将由e.addSuppressed方法追加到原代码异常的后面
- 当然带资源的try/catch会在资源关闭之后执行
- 使用异常机制技巧(早抛出,晚捕获)
- 1-异常不能代替简单的测试(不能将异常用于逻辑判断,性能太差)
- 2-不要过分细化异常(不要把每条可能出现异常的代码装在独立的try块里)
- 3-利用异常层次结构(寻找合适子类或自创的异常,逻辑错误不抛受查异常)
- 4-不要压制异常(吃掉异常不处理)重要的异常一定要处理
- 5-检测到错误及时处理,不要放过,早抛出
- 6-不要羞于传递异常(读一个文件时,出现错误传递异常好过捕获)晚捕获
- 断言允许测试期间向代码中插入检查语句,代码发布时这些插入的检测语句将自动移除
- 语法:
- assert condition(条件(布尔类型的条件表达式)) : error message(字符串类型的错误提示信息);
- 条件为 false 时,抛出断言错误的异常
- 使用场景:
- 1-检查输入参数
- 2-检查返回值
- 3-检查状态或假设
- 4-调试程序
- 断言只用于测试阶段确定程序内部的错误位置,它不改变程序的逻辑运行
- 不要将断言参与业务逻辑处理,实际上也参与不了,断言是致命的不可恢复的错误
- 处理系统错误的三种机制:抛异常、使用断言及记录日志
- 日志可以很容易的定制记录级别、或禁止,也可使用不同处理器进行不同格式的输出
- 并且记录器、处理器都可过滤记录、格式化日志,可配置性很强
- 基本日志:
- 简单使用可以仅使用一个全局日志记录器
- Logger.getGlobal().info("Need to be log...")
- 取消所有日志:
- Logger.getGlobal().setLevel(Level.OFF)
- 高级日志:
- 企业级的日志中,应该自定义日志记录器而不要全部交给全局日志记录器
- 注意:
- 之所以使用static修饰,是为了防止垃圾回收
- 传入的类似包名字符串能很好的使得子包默认继承父包的设置
- 例如:org包设置日志级别,org.package默认与org设置一致
- 日志有7个记录器级别:
- SEVERE、WARNING、INFO、CONFIG、FINE、FINER、FINEST
- 默认值记录前三个级别的日志,调用myLog.setLevel(Level.FINE)可修改默认值
- Level.ALL、Level.OFF为开启、关闭所有日志的特殊常量
- 记录日志有下面几种方法:
- myLog.info(message)方法名指定日志级别
- myLog.log(Level.INFO,message)参数指定日志级别
- 默认将显示日志调用的类名、方法名,虚拟机优化执行时得不到确切调用信息,采用:
- 来获取类和方法的具体位置
- 另外下面这些方法可用来跟踪方法执行流向:
- void entering(String cName, String mName)
- void entering(String cName, String mName, Object p)
- void entering(String cName, String mName, Object[] ps)
- void exiting(String cName, String mName)
- void exiting(String cName, String mName, Object r)
- cName 类名,mName 方法名,p 单参数,ps 参数数组,r 返回值
- 这些调用产生FINER级别、字符串为ENTRY、RETURN开始的日志记录
- 记录异常可使用:
- void throwing(String cName, String mName, Throwable t)
- void log(Level l, String message, Throwable t)
- 日志管理器配置:
- 系统默认配置文件存在于jre/lib/logging.properties文件中
- 想要另外指定配置文件,有以下几种设置方式:
- 虚拟机参数式:-Djava.util.logging.config.file = configFile
- 代码调用式:System.setProperty("java.util.logging.config.file", file)
- 修改默认日志级别,可以在配置文件中加入:
- .level=INFO
- 或者指定自己的记录器的记录级别
- org.package.level=FINE
- 程序运行时,可通过jconsole改变日志的记录级别
- 具体查看 Using JConsole to Monitor Applications
- 日志处理器:
- 默认日志记录器发送到ConsoleHandler,由它输出到System.err流
- 并还将记录发送到父处理器,最终处理器为ConsoleHandler(名称为空,即“”)
- 通过myLog.addHandler(yourHandler)可以安装自己的处理器
- 通过myLog.setUseParentHandlers(false)可以阻止处理器向父处理器传递
- 常用的处理器:
- SocketHandler 将记录发送到特定的主机和端口
- FileHandler 将记录收集到文件当中,默认写到用户主目录 javan.log
- 其中 n 为唯一编号,例如:java001.log 或 java002.log
- 文件处理器的参数及用法,参考API文档
- 日志记录的加工处理
- 过滤器:
- 通过实现Filter接口,实现boolean isLoggable(LogRecord record)
- 格式化:
- 扩展Formatter类,覆写String format(LogRecord record)
- 如果记录有头及尾部时,覆写String getHead(Handler h)和String getTail(Handler h)
- 调试技巧:
- 打印调试信息:
- System.out.println("debug...") 或 Logger.getGlobal().info("...")
- 每个类包含独立main方法,方便单元测试
- JUnit单元测试套件,比前两种更实用
- 匿名代理实现方法调用跟踪
- 打印异常堆栈轨迹:
- throwable.printStackTrace() 或 Thread.dumpStack()
- 错误信息用文件保存:
- java MyProgram 1> errors.txt 2>&1
- 非捕获异常堆栈信息单独处理:
- 虚拟机参数:
- -verbose 能提供虚拟机启动相关信息,排除类加载及路径信息
- -Xlint 告知编译器对代码进行检查,例如:-Xlint:fallthrough(case穿透)
- -Xprof 能剖析代码调用,将分析结果发送到标准输出,获取哪些方法由JIT编译
- 注:-X选项非正式支持,可运行java -X打印非标准参数列表
- jconsole图形工具对应用进行监控和管理,例如:内存、线程使用、类加载等情况
- jmap工具获得堆的快照,jhat加载该快照浏览localhost:70000查看堆内容