美文网首页
Java编程思想总结

Java编程思想总结

作者: AxisX | 来源:发表于2020-11-18 13:48 被阅读0次

    很久没有更新简书了,最近也发生了很多事情,当一切告一段落的时候,继续回炉重造。接下来的半年准备把自己的编程能力进一步深化一下。也由此,重新开始写起了leedcode,细读Java编程思想。
    写完这篇回头看,还是有很多地方写的比较杂乱,之后继续优化。离精通Java道路很长。

    Java是面向对象编程的,也就是说万物皆为对象,每个对象(instance)都有其类型(class),而程序则是对象的集合。对“对象”更简洁的说法是:对象具有状态(对象内部数据)、行为(方法)和标识。“对象”可以理解为其他语言中的变量。而类则描述了具有相同特性和行为的对象集合,所以“类”实际上也是一种数据类型。“对象”是否可以被所有人访问的,考虑这个问题就要引入访问控制。一是让别人无法触及不该触及的部分,二是在改变类的内部时减少其他的影响。访问控制主要由三个关键字在内部设定边界:public、private、protected。后两者的区别在于继承的类可以访问protected成员,但是不能访问private成员。另外还有一种默认的访问权限:包访问权限。说到继承,由基类衍生出导出类(父类与子类)的过程经常由几何形包含圆形、正方形和三角形来举例。这种继承关系可以用"is a"来判断,圆形“is a”几何形,所以圆形是几何形的子类。而其间的差异性则可以覆盖一个新的方法。另外,在面向对象编程中又一个问题是:是否所有的类最终都继承自单一的基类,在Java中这个答案是肯定的,继承自Object类。这样的单根继承结构具有更好的兼容性,也让垃圾回收器的实现容易了许多。

    在对象不再被需要时,需要被清理掉来释放其资源以进行重用,尤其是内存。但是在销毁对象时如何确保其他部分也不再进行调用就需要考虑对象的生命周期。面向对象编程中,有两种方式来创建对象,一个是将对象置于堆栈或静态存储区域内来实现,也就是在编写程序时就确定了对象的存储空间和生命周期。另一种是在被称为堆的内存池中动态地创建对象,在这种方式中,直到运行时才知道需要多少对象和具体的生命周期,是动态内存分配方式。Java就是采用了后者,通过new关键字来构建此对象的动态实例。并采用垃圾回收器处理内存释放的问题。大多数语言都有作用域(scope)的概念,作用域决定了在其内定义的变量名的可见性和生命周期。由花括号{}的位置决定

    操作对象一般是通过“引用”来实现的。例如,String s创建的是一个引用而不是对象,引用要与对象相关联,就需要用到new关键字,从而写成String s=new String("sdfg");。任何东西都涉及到存储问题,一般有五个地方可以用来存储:寄存器(位于处理器内部)、堆栈(位于RAM)、堆(位于RAM)、常量存储(程序代码内部)、非RAM存储(如磁盘)。new关键字就是将对象存储在堆中,除此之外Java将“基本类型”存储在堆栈中,如boolean、char、int等。

    char c='x';
    Character ch =new Character(c);
    
    基本类型

    在此基础上,Java提供了两个用于高精度计算的类:BigInteger和BigDecimal。另外,类作为一种数据类型,也可以通过new来创建。

    Class A{
      int i;//字段,也称数据成员
      double d;
      static int q=23  //static关键字
      function Af(){}//方法,也称成员函数
    }
    A a=new A();
    

    在类的定义中有一个特别的关键字static,无论创建多少个对象(甚至不创建对象)只为特定域分配单一存储空间。并且不与类的任何对象实例相关联。对于Static关键字变量的引用可以通过a.q或者A.q两种,对于非静态成员则不行。对于方法函数也可以同理加上static关键字。一般来说类的代码不是在程序运行时就加载(这种加载通常发生于创建类的第一个对象时),另外当访问static域或者static方法时也会发生加载。定义为static的东西只会被初始化一次。

    在Java中类的初始化是通过构造器来完成的,构造器的名称与类名完全相同。无参的叫做默认构造器,也称无参构造器(如果自己创建的类中没有定义构造器,那么编译器会自动创建一个默认构造器)。构造器也可以加上形式参数。如果想通过多种方式创建一个对象就可以定义多个构造器,它们拥有相同的名字,即类名,但是它们拥有不同的形式参数。这种方式就叫重载。

    //重载demo
    class Tree{
        int height;
        Tree(){
            System.out.println("Planting a seedling");
            height=0;
        }
        Tree(int initialHeight){
            height=initialHeight;
            System.out.println("Creating new Tree that is "+height+" feet tall");
        }
    }
    public class ATest {
        public static void main(String[] args) {
            for(int i=0;i<5;i++){
                new Tree(i);
            }
            new Tree();
        }
    }
    

    对于同一个类的两个对象,同时去调用同一个方法,如果想要进行区分,就要用到this关键字,它只能在方法内部使用,表示对“调用方法的哪个对象”的引用。

    class Tree{
        int height=0;
        Tree increament(){
            height++;
            return this; //this返回对当前对象的引用
        }
        void print(){
            System.out.println("height="+height);
        }
    }
    public class ATest {
        public static void main(String[] args) {
            Tree t=new Tree();
            t.increament().increament().increament().print();
        }
    }
    

    另外this可用于将当前对象传递给其他方法

    class Peeler{
        static Apple peel(Apple apple){
            return apple;
        }
    }
    class Apple{
        Apple getPeeled(){return Peeler.peel(this);}
    }
    

    需要补充的一点是,static方法就是没有this的方法,在static方法内部不能调用非静态方法,它有这全局函数的语义。

    这里特别提一下数组的初始化,一般数组定义为int[] a1;int a1[],前者更能代表它的类型是一个int型数组。但编译器不允许指定数组的大小,一般的定义如下:

    int[] a1={1,2,3};//分配存储空间
    int[] a2;//定义一个数组引用
    a2=a1;//a2和a1成了相同数组的别名
    a2.length;//数组有固有成员length
    Integer[] a=new Interger[rand. nextInt(20)];//非基本类型的数组引用
    for (int i=0;i<a.length;i++){
      a[i]=rand.nextInt(500); //把对象赋值给引用初始化进程完成
    }
    /*花括号列表来初始化数组*/
    Integer[] a={
      new Integer(1),
      new Integer(2),
      3,
    };
    
    Integer[] b=new Integer[]{
      new Integer(1),
      new Integer(2),
      3,
    };
    

    说完初始化,还要说一下销毁。new关键字产生的存储空间是由垃圾回收器来处理的,但是对于其他特殊的内存区域,则需要用到finalize()方法。一旦垃圾回收器准备好释放对象占用的存储空间,首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。但Java中对象是有可能不被垃圾回收的,而且和C++中的析构函数(对象被销毁时自动调用的函数)也不相同(C++中所有对象都会被销毁)。垃圾回收会负责释放对象占据的所有内存,也只与内存有关。finalize常用于对象终结条件的验证。

    protected void finalize(){
      if(checkedOut){
        System.out.println("Error: checked out");
        super.finalize();
      }
    }
    

    垃圾回收技术可以大概理解如下:每个对象都含有一个引用计数器,当有引用连接至对象时,引用计数+1,当引用离开作用域或被置为null时,引用计数减1。垃圾回收器会在含有全部对象的列表上便利,当发现某个对象的引用计数为0时,就释放其占用的空间。但这种方法又一个缺陷,如果对象之间存在循环引用,可能会出现“对象应给被回收,但引用计数却不为0”的情况。定位这种“交互自引用的对象组”况是较为困难的。还有一种垃圾回收计数的思想是:对任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用。反过来讲,如果便利所有的引用,就能找到所有活的对象。

    任何一个Java源代码文件都有个后缀名为.java,此文件通常也被称为编译单元或转译单元,在编译单元内有一个public类,类名与文件名相同,还可能有任意数量的非public类。当编译这个Java文件时,每个类都会有一个输出文件.class。

    上述demo都没有对访问控制进行限制,也就是默认采用包访问权限,即包中所有其他类对该成员都有访问权限,但是此成员对包之外的类来说却是private。包访问权限允许将包内所有相关的类组合起来,其他包内的类想访问该成员,要么该成员未加访问权限修饰词要么该成员为public,或者其他包内的类是该类的继承类则可以访问public/protected,但private不行。还有一种则是该成员提供了访问器accessor和变异器mutator方法(也称作get/set方法),以读取和改变数值,这也是对OOP而言最优雅的方法,也是Javabeans的基本原理。对于关键字private来说,除了包含该成员的类之外,其他任何类都无法访问这个成员,在多线程环境下这个关键字尤为重要。阻止别人直接访问特定的构造器,而需要通过其他方法来进行限制

    class Sundae{
      private Sundae(){}
      static Sundae makeASundae(){  //第一种方式
        return new Sundae();
      }
      private static Sundae ps=new Sundae();//第二种方式,单例模式
      public static Sundae access(){
        return ps;
      }
    }
    public class IceCream{
      public static void main(String[] args){
        Sundae x=Sundae.makeASundae();
      }
    }
    

    关键字protected处理的则是继承的概念。对于类,既不可定义为private也不可定义为protected,只能是包访问权限或public。如果不希望其他任何人对该类拥有访问权限,可以把所有的构造器指定为private,从而阻止任何人创建该类的对象,但是该类的static成员内是可以创建的,如上demo所示。

    代码如果想要复用有两种方式:组合(将对象置于新的类中)和继承。在类的继承中,要了解类的初始化顺序。在我们调用子类的构造器时,Java会自动调用基类的构造器。

    class Tree{
        Tree(){
            System.out.println("Tree constructor");
        }
    }
    class Wood extends Tree{
        Wood(){
            System.out.println("Wood constructor");
        }
    }
    public class ATest extends Wood {
        public ATest(){
            System.out.println("ATest constructor");
        }
        public static void main(String[] args) {
            ATest t=new ATest();
        }
    }
    //output
    Tree constructor
    Wood constructor
    ATest constructor
    

    这是对默认构造器的调用,但是如果要调用带参数的构造器,就需要用到super

    class Tree{
        Tree(int i){
            System.out.println("Tree constructor");
        }
    }
    class Wood extends Tree{
        Wood(int i){
            super(i);
            System.out.println("Wood constructor");
        }
    }
    

    由导出类转型成基类,也可以说子类转成父类的过程,可以称为“向上转型”,是从专用类型向通用类型的转换。而在代码复用的问题上,是采用组合还是继承主要取决于是否需要进行“向上转型”这一操作,如果是需要的,那么应该选择继承,否则可以考虑组合。

    final关键字一般指无法改变的内容。可以用于数据、方法和类中。如果将类定义为final,那么表明该类无法被继承不会有子类。并且类中的所有方法都隐式定义为final,无法对其进行覆盖。final类中如果对方法添加final关键字不会有实际意义。并且一个普通类中所有的private方法其实也隐式地指定为final,由于无法取用private方法也就无法覆盖它。那么对private方法添加final关键字也没有实际意义。

    import java.util.Random;
    
    class Value{
        int i;
        public Value(int i){
            this.i=i;
        }
        public Value(){
            System.out.println("Value");
        }
    }
    public class ATest{
        private static Random rand=new Random(47);
        private String id;
        private final int j;//black final,should init before used
        public ATest(String id){
            j=1;//initialize black final
            this.id=id;
        }
        private final int valueOne=9;
        private static final int VALUE_TWO=99;
        public static final int VALUE_THREE=39;
        private final int i4=rand.nextInt(20);
        static final int INT_5=rand.nextInt(20);
        private Value v1=new Value(11);
        private final Value v2=new Value(22);
        private static final Value VAL_3=new Value(33);
        private final int[] a={1,2,3,4,5,6};
        public String toString(){
            return id+":"+"i4="+i4+".INT_5="+INT_5;
        }
        public int with(final int k){//定义参数为final
            return k+1;//方法中无法改变参数所指的对象和其值
        }
    
        public static void main(String[] args) {
            ATest fd1=new ATest("fd1");
            fd1.v2.i++;
            fd1.v1=new Value(9);
            for(int i=0;i<fd1.a.length;i++){
                fd1.a[i]++;
            }
            System.out.println(fd1);
            System.out.println("Creating new ATest");
            ATest fd2=new ATest("fd2");
            System.out.println(fd1);
            System.out.println(fd2);
        }
    }
    

    Java中还有个很重要的概念叫多态。Shape基类提供一个公共接口,所有的形状导出类都可以描绘和擦除。导出类通过覆盖这些定义,为每种特殊几何形状提供单独的行为。RandomShapeGenerator类提供了next函数,通过return将Circle/Square/Triangle引用向上转型成一个通过的Shape引用。

    import java.util.Random;
    
    class Shape{
        public void draw(){}
        public void erase(){}
    }
    class Circle extends Shape{
        public void draw(){System.out.println("Circle.draw()");}
        public void erase(){System.out.println("Circle.erase()");}
    }
    class Square extends Shape{
        public void draw(){System.out.println("Square.draw()");}
        public void erase(){System.out.println("Square.erase()");}
    }
    class Triangle extends Shape{
        public void draw(){System.out.println("Triangle.draw()");}
        public void erase(){System.out.println("Triangle.erase()");}
    }
    class RandomShapeGenerator{
        private Random rand=new Random(47);
        public Shape next() {
            switch (rand.nextInt(3)){
                default:
                case 0:return new Circle();
                case 1:return new Square();
                case 2:return new Triangle();
            }
        }
    }
    public class ATest{
        private static RandomShapeGenerator gen=new RandomShapeGenerator();
    
        public static void main(String[] args) {
            Shape[] s=new Shape[9];
            for (int i=0;i<s.length;i++){
                s[i]=gen.next();
            }
            for (Shape shp:s){
                shp.draw();
            }
        }
    }
    //output
    Triangle.draw()
    Triangle.draw()
    Square.draw()
    Triangle.draw()
    Square.draw()
    Triangle.draw()
    Square.draw()
    Triangle.draw()
    Circle.draw()
    

    但是如果直接访问某个域,即将Sub对象转型为Super引用时,域访问操作都是由编译器解析的,不属于多态。在引用Sub重的field时所产生的默认域非Super版本的field域,如果想得到的时Super.field就需要显示引用super.field。

    class Super{
        public int field=0;
        public int getField(){return field;}
    }
    class Sub extends Super{
        public int field=1;
        public int getField(){return field;}
        public int getSuperField(){return super.field;}
    }
    
    public class ATest{
        public static void main(String[] args) {
            Super sup=new Sub();
            System.out.println("sup.field="+sup.field+".sup.getField()="+sup.getField());
            Sub sub=new Sub();
            System.out.println("sub.field="+sub.field+".sub.getField()="+sub.getField()+".sub.getSuperField()="+sub.getSuperField());
    
        }
    }
    

    对于构造器有如下的demo

    class Meal{
        Meal(){System.out.println("Meal()");}
    }
    class Bread{
        Bread(){System.out.println("Bread()");}
    }
    class Cheese{
        Cheese(){System.out.println("Cheese");}
    }
    class Lettuce{
        Lettuce(){System.out.println("Lettuce");}
    }
    class Lunch extends Meal{
        Lunch(){System.out.println("Lunch()");}
    }
    class PortableLunch extends Lunch{
        PortableLunch(){System.out.println("PortableLunch()");}
    }
    public class ATest extends PortableLunch{
        private Bread bread=new Bread();
        private Cheese cheese=new Cheese();
        private Lettuce lettuce=new Lettuce();
        public ATest(){System.out.println("ATest()");}
    
        public static void main(String[] args) {
            new ATest();
        }
    }
    //output
    Meal()
    Lunch()
    PortableLunch()
    Bread()
    Cheese
    Lettuce
    ATest()
    

    根据output可以看出首先调用的是基类构造器,然后再找下一层导出类,直到最底层的导出类。然后按照声明顺序调用成员的初始化方法,最后调用导出类构造器的主体。下面这个程序也值得debug看一下调用顺序,另外会发现如果子对象依赖于其他对象, 那么销毁顺序会和初始化顺序相反。

    class Characteristic{
        private String s;
        Characteristic(String s){
            this.s=s;
            System.out.println("Creating Characterstic"+s);
        }
        protected void dispose(){
            System.out.println("disposing Characterstic"+s);
        }
    }
    class Description{
        private String s;
        Description(String s) {
            this.s=s;
            System.out.println("Creating Description"+s);
        }
        protected void dispose(){
            System.out.println("disposing Description"+s);
        }
    }
    class LivingCreature{
        private Characteristic p=new Characteristic("is alive");
        private Description t=new Description("Basic Living Creature");
        LivingCreature(){
            System.out.println("Living Creature()");
        }
        protected void dispose(){
            System.out.println("LivingCreature dispose");
            t.dispose();
            p.dispose();
        }
    }
    class Animal extends LivingCreature{
        private Characteristic p=new Characteristic("has heart");
        private Description t=new Description("Animal not Vegetable");
        Animal(){System.out.println("Animal()");}
        protected void dispose(){
            System.out.println("Animal dispose");
            t.dispose();
            p.dispose();
            super.dispose();
        }
    }
    class Amphibian extends Animal {
        private Characteristic p = new Characteristic("can live in water");
        private Description t = new Description("Both water and land");
    
        Amphibian() {
            System.out.println("Amphibian");
        }
    
        protected void dispose() {
            System.out.println("Amphibian dispose");
            t.dispose();
            p.dispose();
            super.dispose();
        }
    }
    public class ATest extends Amphibian{
        private Characteristic p=new Characteristic("Croaks");
        private Description t=new Description("Eat Bugs");
        public ATest(){System.out.println("Frog()");}
        protected void dispose(){
            System.out.println("ATest dispose");
            t.dispose();
            p.dispose();
            super.dispose();
        }
    
        public static void main(String[] args) {
            ATest aTest=new ATest();
            System.out.println("Bye");
            aTest.dispose();
        }
    }
    //output
    Creating Charactersticis alive
    Creating DescriptionBasic Living Creature
    Living Creature()
    Creating Characterstichas heart
    Creating DescriptionAnimal not Vegetable
    Animal()
    Creating Charactersticcan live in water
    Creating DescriptionBoth water and land
    Amphibian
    Creating CharactersticCroaks
    Creating DescriptionEat Bugs
    Frog()
    Bye
    ATest dispose
    disposing DescriptionEat Bugs
    disposing CharactersticCroaks
    Amphibian dispose
    disposing DescriptionBoth water and land
    disposing Charactersticcan live in water
    Animal dispose
    disposing DescriptionAnimal not Vegetable
    disposing Characterstichas heart
    LivingCreature dispose
    disposing DescriptionBasic Living Creature
    disposing Charactersticis alive
    

    如果基类中的方法都是未实现的方法,那该基类的目的就是为它所有导出类创建一个通用接口,该类可以称为抽象基类或简称为抽象类。相应的,Java为其提供了名为“抽象方法”的机制,这种方法是不完整的,仅有声明而没有方法体abstract void f()。包含抽象方法的类也就称为抽象类,反过来说,如果一个类包含一个或多个抽象方法,该类必须被限定为抽象的,否则编译器会报错。另外如果想让一个类直接成为接口类,在前面加上interface关键字即可。接口是实现多重继承的途径,而生成遵循某个接口的对象的典型方式就是工厂。

    要创建任意数量的对象可以用数组ArrayList,但是它有固定的尺寸限制,Java还提供了其他容器类,如List、Set、Queue、Map,这些对象类型也称为集合类。对于数组来说, 一般来讲定义如ArrayList apples=new ArrayList(),如果想指定类型,可以定义为ArrayList<Apple> apples=new ArrayList<Apple>()。List必须按照插入顺序保存元素,而Set不能有重复元素。Queue则按排队的规则来确定对象产生的顺序。Map则是键值对。也被称为字典。List包括ArrayList和LinkedList,它们都是顺序插入,不同在于,LinkedList包含的操作多于ArrayList。ArrayList擅长随机访问元素,但在List中间插入和移除元素较慢。LinkedList则相反,在顺序访问方面较快,它一般也作为栈来使用。Set则包含了HashSet、TreeSet、LinkedHashSet,HashSet是获取元素最快的方式,不关心存储顺序。而TreeSet则注重存储顺序,按比较结果升序保存对象,LinkedHashSet则是按照被添加的顺序保存对象。另外,Map的保存顺序并不是插入顺序。

    import java.util.*;
    
    class PrintingContainers{
        static Collection fill(Collection<String> collection){
            collection.add("rat");
            collection.add("cat");
            collection.add("dog");
            collection.add("dog");
            return collection;
        }
        static Map fill(Map<String,String> map){
            map.put("rat","Fuzzy");
            map.put("cat","Rags");
            map.put("dog","Bosco");
            map.put("dog","Spot");
            return map;
        }
    
        public static void main(String[] args) {
            System.out.println(fill(new ArrayList<String>()));
            System.out.println(fill(new LinkedList<String>()));
            System.out.println(fill(new HashSet<String>()));
            System.out.println(fill(new TreeSet<String>()));
            System.out.println(fill(new LinkedHashSet<String>()));
            System.out.println(fill(new HashMap<String, String>()));
            System.out.println(fill(new TreeMap<String, String>()));
            System.out.println(fill(new LinkedHashMap<String, String>()));
        }
    }
    //output
    [rat, cat, dog, dog]
    [rat, cat, dog, dog]
    [cat, dog, rat]
    [cat, dog, rat]
    [rat, cat, dog]
    {cat=Rags, dog=Spot, rat=Fuzzy}
    {cat=Rags, dog=Spot, rat=Fuzzy}
    {rat=Fuzzy, cat=Rags, dog=Spot}
    

    对于不同类型的容器,想要做到通用,就需要用到迭代器Iterator,迭代器是轻量级对象,创建的代价较小,但是只能单向移动,通过next来获取下一个元素,hasNext来检查序列中是否有某元素,remove用来移除。Iterator有很多子类型,例如ListIterator只能用于各种List类的访问,但它可以双向移动。

    在使用栈的时候经常可以看到类似如下的定义:

    public class Stack<T>{
      private LinkedList<T> storage=new LinkedList<T>();
      public void push(T v){storage.addFirst(v);}
      public T peek(){return storage.removeFirst();}
      public T pop(){return storage.removeFirst();}
    }
      public boolean empty(){return storage.isEmpty();}
      public String toString(){return storage.toString();}
    

    类名之后的<T>是告诉编译器这是一个参数化类型,而其中的类型参数,即在类被使用时将会被实际类型替换的参数,就是T。LinkedList也能支持队列的行为,并且实现了Queue接口。Queue相关的offer方法是将一个元素插入到队尾,peek和element都在不移除的情况下返回队头

    import java.util.LinkedList;
    import java.util.Queue;
    import java.util.Random;
    
    public class ATest{
        public static void printQ(Queue queue){
            while(queue.peek()!=null){
                System.out.println(queue.remove()+" ");
            }
            System.out.println();
        }
        public static void main(String[] args) {
            Queue<Integer> queue=new LinkedList<Integer>();
            Random random=new Random(47);
            for (int i=0;i<10;i++){
                queue.offer(random.nextInt(i+10));
            }
            printQ(queue);
            Queue<Character> qc=new LinkedList<Character>();
            for (char c:"Brontosaurus".toCharArray()){
                qc.offer(c);
            }
            printQ(qc);
        }
    }
    

    还有一种队列类型是PriorityQueue,弹出元素是具有最高优先级的元素。如果想改变顺序可以提供自己的Comparator。所有描述序列容器的共性接口叫做Collection,而实现Colletion的同时就要提供Iterator方法。容器的分类如下:


    容器分类

    之前这些都是顺序编程,最后再说说并发。并发主要解决速度和设计可管理性两方面。Servlet就自带多线程特性。并发虽然增加了上下文切换的代价,但是当有任务/线程阻塞时,可以保证其他任务继续执行。实现并发最直接的方式实在操作系统级别使用进程。进程是运行在自己的地址空间内的自包容的程序,所以它们也是各自独立的。但是进程一般是有数量和开销限制的,所以Java在顺序型语言基础上提供了对线程的支持。线程机制是由执行程序表示的单一进程中创建任务。它是抢占式的,也就是说调度机制会周期性地中断线程,将上下文切换到另一个线程,从而为每个线程都提供时间片,使得每个进程都会分配到合理数量的时间去驱动它的任务。一般多线程系统会把可用线程数量控制在相对较小的数字,而解决这个问题的办法就是引入协作多线程。在协作系统中,每个任务都自动放弃控制,插入某种类型的让步语句。在任务彼此间协作的问题上,并发库提供了await()、signal()方法的Condition对象。wait()会在等待外部世界产生变化的时候将任务挂起,并且只有在notify()和notifyAll()发生时,即表示发生了某些感兴趣的事物,这个任务才会被唤醒并去检查所发生的变化。wait()会将锁释放,该对象重的其他synchronized方法可以在wait()期间被调用。
    线程可以驱动任务,因此引入Runnable接口来描述任务。

    import java.util.concurrent.Executor;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class ATest implements Runnable{//从Runnable导出类必须有run方法
        protected  int countDown=10;
        private static int taskCount=0;
        private final int id=taskCount++;
        public ATest(){}
        public ATest(int countDown){this.countDown=countDown;}
        public String status(){
            return "#"+id+"("+(countDown>0?countDown:"LiftOff!")+").";
        }
        public void run(){//run方法中写成无限循环形式
            while (countDown-- >0){//跳出循环的条件
                System.out.println(status());
                Thread.yield();
            }
        }
        public static void main(String[] args) {
    //        part1
    //        Thread thread=new Thread(new ATest());//将Runable对象转变成工作任务就是交给Thread构造器
    //        thread.start();
    //        System.out.println("Waiting for LiftOff");
    //        ATest aTest=new ATest();
    //        aTest.run();
            ExecutorService executorService= Executors.newCachedThreadPool();//Executor执行期管理Thread对象,ExecutorService则是具有服务生命周期的Executor
            ExecutorService executorService1=Executors.newFixedThreadPool(5);//FixedThreadPool使用有限线程集来执行所提交的任务
            for (int i=0;i<5;i++){
    //            part2
    //            new Thread(new ATest()).start();
                executorService.execute(new ATest());
    
            }
            executorService.shutdown();
    
        }
    }
    

    如果想要获得返回值,就不能实现Runnable接口而要实现Callable接口。

    import java.util.ArrayList;
    import java.util.concurrent.*;
    
    public class ATest implements Callable {//Callable具有类型参数的范型
        private  int id;
        public ATest(){}
        public ATest(int id){this.id=id;}
    
        public String call(){
            return "result of TaskWithResult"+id;
        }
        public static void main(String[] args) {
            ExecutorService executorService= Executors.newCachedThreadPool();//Executor执行期管理Thread对象,ExecutorService则是具有服务生命周期的Executor
            ArrayList<Future<String>> result=new ArrayList<Future<String>>();
            for (int i=0;i<5;i++){
                result.add(executorService.submit(new ATest(i)));//submit方法调用call
    
            }
            for(Future<String> fs:result){
                try{
                    System.out.println(fs.get());
                }
                catch (InterruptedException e){
                    System.out.println(e);
                    return;
                }
                catch (ExecutionException e){
                    System.out.println(e);
                }
                finally {
                    executorService.shutdown();
                }
            }
        }
    }
    

    一般调度器会让优先级最高的线程先执行,在绝大多数时间中,所有线程都按默认优先级运行。可以通过getPriority()来读取现有线程的优先级,也可以在任何时刻通过Thread.currentThread().setPriority(priority)来修改优先级。yield方法的调用是对线程调度器的表明“已经执行完生命周期最重要的不分了,可以切换给其他任务执行一段时间”。程序中一般还运行这一些为后台提供通用服务的线程,即“后台线程”(daemon),并且这种线程不属于程序中不可或缺的部分。也就是说当非投胎线程结束时,程序也会终止,不管是否还存在后台进程。但线程之间也可能存在冲突,此时就要引入“锁”的概念。Java以关键字synchronized的形式,为防止资源冲突提供内置的支持。当任务要执行被关键字synchronized关键字保护的代码片段的时候,它将检查锁是哦服可用,然后获取锁,执行代码,再释放锁。共享资源一般是以对象形式存在的内存片段,要对其访问控制,首先要把它包进一个对象,然后把所有要访问这个资源的方法标记为synchronized,如果某个任务处于一个对标记为synchronized方法的调用中,那么这个线程从该方法返回之前,其他所有要调用类中任何标记为synchronized方法的线程都会被阻塞。

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.TimeUnit;
    
    class Meal{
        private final int orderNum;
        public Meal(int orderNum){this.orderNum=orderNum;}
        public String toString(){return "Meal"+orderNum;}
    }
    
    class WaitPerson implements Runnable{
        private ATest aTest;
        public WaitPerson(ATest a){aTest=a;}
        public void run(){
            try{
                while(!Thread.interrupted()){
                    synchronized (this){
                        while(aTest.meal==null){
                            wait();
                        }
                    }
                }
                System.out.println("Waitperson got"+aTest.meal);
                synchronized (aTest.chef){
                    aTest.meal=null;
                    aTest.chef.notifyAll();
                }
            }
            catch (InterruptedException e){
                System.out.println("WaitPerson interrupted");
            }
        }
    }
    class Chef implements Runnable{
        private ATest aTest;
        private int count=0;
        public Chef(ATest a){aTest=a;}
        public void run(){
            try{
                while(!Thread.interrupted()){
                    synchronized (this){
                        while (aTest.meal!=null){
                            wait();
                        }
                    }
                    if (++count==10){
                        System.out.println("Out of food,closing");
                        aTest.executorService.shutdownNow();
                    }
                    System.out.println("Order up!");
                    synchronized (aTest.waitPerson){
                        aTest.meal=new Meal(count);
                        aTest.waitPerson.notifyAll();
                    }
                    TimeUnit.MICROSECONDS.sleep(10);
                }
            }
            catch (InterruptedException e){
                System.out.println("Chef interrupted");
            }
        }
    }
    public class ATest{
        Meal meal;
        ExecutorService executorService= Executors.newCachedThreadPool();
        WaitPerson waitPerson=new WaitPerson(this);
        Chef chef=new Chef(this);
        public ATest(){
            executorService.execute(chef);
            executorService.execute(waitPerson);
        }
    
        public static void main(String[] args) {
            new ATest();
        }
    }
    

    枚举类型由关键字enum表示,可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为程序的组件来使用。调用enum的value方法,可以遍历enum实例。

    enum Shrubbery{GROUND,CRAWLING,HANGING}
    
    public class ATest{
        public static void main(String[] args) {
            for (Shrubbery s:Shrubbery.values()){
                System.out.println(s +"ordinal"+s.ordinal());//返回每个实例在声明时的次序
                System.out.println(s.compareTo(Shrubbery.CRAWLING)+" ");//Enum类实现了Comparable接口所以具有compareTo方法,并且实现了Serializable接口
                System.out.println(s.equals(Shrubbery.CRAWLING)+" ");
                System.out.println(s==Shrubbery.CRAWLING);
                System.out.println(s.getDeclaringClass());//得到所属类
                System.out.println(s.name());//返回实例声明时的名字
                System.out.println("--------------");
            }
            for (String s:"HANGING CRAWLING GROUND".split(" ")){
                Shrubbery sh=Enum.valueOf(Shrubbery.class,s);
                System.out.println(sh);
            }
        }
    }
    

    最后进行一下梳理。
    (1)Java集合。Java集合包含List、Set、Map、Queue等。
    List包含ArrayList、LinkedList、Vector等。ArrayList基于数组实现,初始化容量为10,当容量不足时可以扩容1.5倍。第一次调用add时分配10个内存空间,之后按1.5倍增加,即15个内存空间、22个内存空间。LinkedList代表队列,是一种特殊的线性表,只允许在表的前段进行删除,表的后端进行插入。LinkedList类实现了Queue接口,因为LinkedList也可以当成Queue来用。Queue queue=new LinkedList<>();它也分为大小根堆实现。Vector也是List接口的典型实现,它的线程是不安全的,Vector线程是安全的
    Set包含HashSet、TreeSet、LinkedHashSet等。HashSet按照Hash算法存储集合中的元素,基于HashMap实现,它不能保证元素的排列顺序,不可重复(hashcode返回值重复&&equals方法值相等),可以null,线程不安全。当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据hashCode值来决定该对象在HashSet中的存储位置。如果两个元素的equals方法返回true,但他们的hashCode返回值不等,也可以被成功添加。这里可以注意一下Obejct中,HashCode和equals方法的默认实现。TreeSet则包含了排序,基于红黑树实现。它调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系然后升序排列。this>obj返回1,this<obj返回-1,this=obj,返回0,认为两个相等。这个函数必须放入同类的对象或用泛型,否则会产生异常。
    Map包含HashMap、HashTable、TreeMap、LinkedHashMap、ConcurrentHashMap等。HashMap和刚才说的HashSet类似,它有很多常见问题,首先它的容量默认为16,每次增加都是2的幂次方,最大容量是2^30,HashTable和它相反:HashTable线程安全,不允许使用null作为key。TreeMap是保证所有的Key-Value处于有序状态,自然排序条件是所有的Key必须实现Comparable接口,且所有的Key是同一类对象。定制排序条件是传入一个Comparator对象,该对象负责对TreeMap中所有的key做排序。LinkedHash继承自HashMap,相比于HashMap增加了双向链表头节点header和标志位accessOrder,标志位的值为true时表示按顺序迭代,值为false时表示按插入顺序迭代。另外,它重新定义了Entry,增加了before、after两个指针。而Entry之间的连接顺序则用next连接。另外,它还对addEntry、recordAccess、get等操作进行了充血。
    Queue可以用LinkedList实现,还可用BlockingQueue实现并发,其中包含ArrayBlockQueue、LinkedBlockingQueue、PriorityBlockingQueue、DelayQueue、SynChronousQueue、LinkedTransferQueue、LinkedBlockingDeque。

    (2)面向对象。Java作为面向对象的语言,包含了封装、继承、多态等特征。
    封装。它使数据和加工该数据的方法称为一个整体,用户只能看见对象的外特性,而对象的私有数据、实现方法等对用户不可见。将设计和使用进行分开。
    继承。继承是类的派生功能的体现。子类可以继承父类数据和方法,同时可以修改或者扩充。继承还具有传递性,分为单继承(一子一父)和多继承(一子多父),因为类的封装特性,如果没有继承机制,类的对象中的数据和方法就会出现大量的重复。
    多态。多态是对象根据接收的信息而做出的动作。例如,不同的动物对象调用run方法,有这不同的功能,鸟的run是飞,狮子的run是跑。多态的实现由继承来支持,利用类继承的层次关系,把具有通用功能的放在高层,功能实现的不同方法放在底层。Java中多态也通过方法的重载和重写来实现。具体的实现方法值得研究。

    Java类与类之间也有多种关系,继承、依赖、关联、聚合、组合等。继承是is a的关系。依赖就是类A中的方法使用了类B。关联是两个类或类与接口之间语义级别的一种强依赖关系,一般通过成员变量来实现。被关联类B以类属性的形式出现在关联类A中,或者关联类A引用了一个类习性为被关联类B的全局变量。聚合则体现的是整体与部分、拥有的关系,即has-a。在代码层面,聚合和关联基本一致,但是语义级别不同,其整体与部分可分隔,部分可以属于多个整体。组合则是关联的一种特例,体现的是contains a,这种关系比聚合更强,也称为强聚合,整体和部分不可分隔,其生命周期相同。

    另外要说一下对象的初始化过程:先加载父类class,再加载子类class(静态代码块在加载class文件的时候执行一次),在栈中申请空间,声明子类变量。在堆内存中开辟空间,分配地址和内存。在对象空间中,对对象的属性(包括父类的属性)进行默认初始化。子类构造函数进栈,显示初始化父类的属性,执行父类中的代码块,然后执行父类的构造函数。父类构造方法进栈,执行完毕后出栈。显示初始化子类的属性,执行子类中的代码块,然后执行子类的构造函数。初始化完毕后,将堆内的地址赋值给引用变量,子类构造方法出栈。

    (3)泛型。Java泛型可以解决数据类型安全性的问题,主要原理是在类声明是,通过一个标识表示类中某个属性的类型或者某个方法的返回值及参数类型。泛型只在编译阶段有效,在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦除,并在对象进入和离开方法边界处添加类型检查和类型转换方法,泛型信息并不会进入到运行阶段。泛型通用的通配符有以下几种

    <? extends Person>  //只允许泛型为Person及Person子类的引用调用
    <? super Person> //只允许泛型为Person及Person父类的引用调用
    <? extends Comparable> //只允许泛型为实现Comparable接口的实现类的引用调用
    

    (4)反射机制。Java反射的前提是已经加载过此类,可以通过类名来寻找这个类的相关信息。反射机制可以在运行时判断一个对象所属的类、构造任意一个类的对象、判断一个类所具有的成员变量和方法、调用任意一个对象的成员变量和方法、生成动态代理。还通过调用类中指定的方法可以调用指定的属性。
    通过反射获取类的完整结构可以通过以下方式:

    Field
    Method
    Constructor
    Superclass
    Interface
    Annotation
    

    (5)代理。代理的实现可以让客户与委托类之间解耦,在不修改委托类代码的情况下能够做一些额外的处理。代理也分为静态代理、动态代理等。静态代理的代理类在程序运行前就已经存在,其代理类和委托类会实现同一接口或派生自相同的父类,过程可以通过聚合实现,让代理类持有一个委托类的引用。动态代理是在运行时根据Java代码中的“指示”动态生成的,而不是在Java代码中定义的。相比于静态代理,动态代理的优势在于可以对代理类的函数进行同一管理,不用修改每个代理类的函数。动态代理的实现机制就是通过Java反射机制来实现,对一个已知对象在运行时动态调用其方法。

    (6)并发
    并发有个关键字volatile。它的作用是保证变量可见性,禁止指令重排序,还是一种比synchronized更轻量级的锁。在对volatile变量修饰的共享变量进行写操作时,会在对应的汇编代码中加入lock指令。该指令会将当前处理器缓存数据写回系统内存,写回系统内存的操作会使其他CPU里缓存该地址的数据无效。读取volatile变量时,线程对应本地内存置为无效,线程从主内存读取共享变量。写volatile变量时,其他CPU里缓存了该内存地址的数据无效。volatile关键字对读写操作有原子性,而对复合操作没有。还要注意的是,volatile关键字进行重排序。该关键字一般用于一个变量被多线程共享,且线程直接给变量赋值,不适用于复合操作。
    并发还有个关键字synchronized,它是一个同步锁,当多个线程同时访问一段同步代码时,首先会进入_EntryList集合,当线程获取到对象的monitor后进入_Owner区域并把monitor重的owner变量设置为当前线程,同时monitor中的计数器count+1。若线程调用wait方法,就会释放当前持有的monitor,owner恢复为null,count-1,同时该线程进入WaitSet中等待被唤醒,若当前线程执完毕也将释放monitor锁并复位变量的值,以便其他线程进入,获取monitor(锁)。任何一个对象都有monitor相关联。synchronized有三种形式,普通方法锁住的是当前对象,静态方法锁住的是当前类的对象,代码段则锁住的是synchronized括号里配置的对象。锁也有多种状态:CG标记11、偏向锁01、轻量级锁00、重量级锁10.升级的方式是从GC到重量级锁逐步递升。

    (7)多线程
    多线程主要依靠Thread类。

    Thread() 创建Thread对象
    Thread(String threadname) 创建线程并指定线程实例名
    Thread(Runnable target) 指定创建线程的目标,实现了Runable接口中的run方法,创建新的Thread对象
    Thread(Runable target, String name) 创建新的Thread对象
    

    还会用到ThreadLocal类,变量为当前线程,该变量对于其他线程而言是隔离的,为每个线程创建一个副本,即每个线程可以访问自己内部的副本变量。它多应用于对象跨层传递、线程间数据隔离、事务操作(存储线程事务信息)、数据库连接(Session会话管理)

    (8)垃圾回收机制
    垃圾回收机制分为新生区(1/3堆空间)、老年区(2/3堆空间)和元数据区(直接内存)。新生区是类诞生、应用和消亡的区域,其中又分为伊甸区(Eden space,占8/10)和幸存者区(Survivor space,分为幸存0区和1区,分别占1/10)。所有的类都是在Eden区被创建,如果该区被用完但是还需要创建对象,就需要垃圾回收器堆该区域进行垃圾回收,区域内不再被其他对象所引用的对象进行销毁,然后将Eden区剩余对象易到幸存0区。如果0区也满了,就再堆该区进行垃圾回收,剩余的放到1区。如果1区也满了就再进行垃圾回收,放到0区中,如此反复轮询交替15次后,剩下的对象就会放到老年区。也就是说老年区存放的是新生区经过多次GC依然存活的对象,如果老年区也满了就会产生MajorGC(FullGC)进行老年区的内存清理,若老年区执行了Full GC之后发现依然无法进行对象的保存就会产生OOM(OutOfMemoryError)异常。但是FullGC尽量少用,早期它会把程序全部停掉来GC,即STW(Stop The World),现在则是并发GC和程序本身。前两个区属于JVM,但是元数据区不属于,它是直接本地物理内存,对应着Java8以前的永久代,是JVM中的非堆区。元数据区也可能发生OutOfMemory异常。
    垃圾回收过程中采用垃圾回收算法来判断对象是否存活,主要又引用计数器算法和根搜索法。引用计数器算法是给每个对象设置一个计数器,当有其他地方引用这个对象的时候,计数器+1,引用失效时计数器-1,计数器为0时,JVM认为对象不再被使用,是垃圾了。但是该算法不能解决循环引用的问题,即A对象引用B对象,而B对象又引用了A对象,而AB对象已不被其他对象引用。同时每次计数器的增加和减少都带来了额外的开销,所以在JDK1.1之后引用计数器算法就不再被使用了。
    根搜索算法也叫可达性分析法,通过一些GCRoots对象作为起点,从这些节点往下开始搜索,搜索通过的路径称为引用链ReferenceChain,当一个对象没有被GCRoots的引用链表连接的时候就说明这个对象是不可用的。GCRoots对象包括:虚拟机栈中的引用的对象,方法区域中的类静态属性引用对象、方法区域中常量引用的对象、本地方法栈中JNI(Native方法)的引用的对象。对于对象的引用也分为强引用、软引用、弱引用、虚引用。强引用是垃圾回收器永远不会回收的对象,软引用是有用但非必须的对象,在系统发生内存溢出之前,会把对象列为回收范围进行回收,如果此次回收还没有足够的内存,才会抛出内存溢出异常。弱引用是非必须的对象,只能留存到下一次垃圾回收机制发生之前,JDK提供了WeakReference类来实现弱引用,无论当前内存是否足够,软引用相关联的对象都会被回收。虚引用也被称为幽灵引用或幻影引用,JDK提供了PhantomReference类来实现虚引用,为一个对象设置虚引用是为了能在该对象在垃圾回收器回收时收到一个通知。
    而一个对象是否应该在垃圾回收器回收,需要经过两次标记过程。第一次通过可达性分析算法分析对象是否与GC Roots可达,经过第一次标记,被筛选为不可达的对象会进行第二次标记。第二次标记过程会判断不可达对象是否有必要执行finalize方法。执行条件是当前对象的finalize方法被重写,并且还未被系统调用过。如果允许执行那么这个对象将会被放到一个叫F-Query的队列中,等待被执行。
    真正的回收过程也有相应的算法,如标记-清除法、标记整理算法、复制算法、分代收集算法。
    标记-清除法将垃圾回收分为两个阶段:标记阶段和清除阶段。标记阶段首先通过根节点,标记所有从根节点开始的可达对象,因此未被标记的对象就是未被引用的垃圾对象,然后在清除阶段,清除所有未被标记的对象。但是这种算法效率较低,还会产生大量不连续的内存碎片。
    标记整理法对标记完的对象不直接清理,而是让所有存活的对象都向一段移动,然后直接清理掉边界以外的内存。但是效率也不高。
    复制法是将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还或者的对象复制到另一块生面,然后再把已经使用过的内存空间一次清理掉,这样使得每次都是对整个半区进行内存回收。内存分配时也不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,这种方式效率有所提升,且没有内存碎片,但是浪费一般的内存空间,而且对象存活率较高时,复制操作也多,效率也一般。
    分代收集算法根据对象存活周期的不同将内存划分为几块,一般是把Java堆分成新生代和老年代,然后根据各个年代的特点采用适当的收集算法,在新生代中,每次垃圾收集都发现有大批对象死去,只有少量存活,就选用复制算法,而老年代因为对象存活率高,没有额外空间对它进行分配担保,就必须采用标记清理或标记整理方法来进行回收。
    垃圾回收器分为:Serial收集器(单线程,新生代采用复制算法,老年代采用标记整理法,进行垃圾收集时所有其他工作线程暂停)、ParNew收集器(Serial收集器的多线程版本)、Parallel Scavenge收集器(新生代收集器,目标是达到一个可控制的吞吐量)、Serial Old收集器(老年代收集器,单线程,采用标记整理法,JDK1.5之前与Parallel Scavenge配合使用。作为CMS收集器的后备预案)、Parallel Old收集器(Parallel Scavenge收集器的老年代版本,采用多线程和标记整理法,与Parallel Scavenge配合使用)、CMS收集器(以获取最短回收停顿时间为目标的收集器,重视服务端响应速度,希望停顿时间最短)、G1垃圾收集器。这里多说一句CMS收集器,它分为初始标记(STW,只标记GC Roots相关对象)、并发标记(进行GC Roots Tracing过程)、重新标记(修正并发标记期间,因用户程序继续运行而导致标记变动那一部分对象的标记记录,STW)、并发清除(清理垃圾对象,耗时长,但与用户线程并发执行)。

    (9)JVM
    JVM主要分为两部分:内存模型和类加载机制。
    内存模型主要包含运行时数据区、类加载子系统、垃圾收集器、执行引擎。
    运行时数据区分为堆、Java栈、方法区、本地方法栈、程序计数器。
    堆由虚拟机启动时创建,用于存放对象实例,几乎所有对象(包括常量池)都在堆上分配对象,当对象无法在空间中申请到内存,就会抛出OutOfMemoryError异常。堆也是垃圾回收器管理的主要区域,可通过-Xmx或-Xms来指定最大堆和最小堆。
    堆是由线程共享的,Java栈则是线程独有的。Java栈是线程执行方法的内存模型,每个方法都在执行的同时创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,线程执行结束就释放栈。Java栈有可能出现两种异常,一个是StackOverFlowError(线程请求的栈深度大雨虚拟机栈允许的深度),另一个是OutOfMemoryError(若虚拟机栈可动态扩展,当无法申请到足够内存空间时将抛出此异常)
    方法区也是线程共享的,存放有静态变量、常量和类相关信息(构造方法/接口定义等),另外运行时常量池也在方法区。它可能包含的问题是OutOfMemoryError。本地方法栈则是线程独有的,可能出现的问题包括OutOfMemoryError和StackOutFlowError。程序计数器也是线程独有的,一个指针指向方法区中的方法字节码,即存储下一条指令的地址,由执行引擎读取下一条指令,是非常小的内存空间。但是程序计数器不涉及到内存异常。
    Java内存异常主要包括内存泄漏和内存溢出。
    内存泄漏。如HashMap、LinkedList等静态类,生命周期与程序一致,容器中的对象在程序结束之前如果没有被释放就会造成内存泄漏。或者数据库访问过后连接未关闭,也会造成大量对象无法被回收。还有,一个变量的定义的作用范围大于其使用范围也可能造成内存泄漏。另外,内部类中持有外部类、改变哈希值、过期引用、缓存泄漏、监听器和回调等都可能造成内存泄漏。
    内存溢出是内存中加载的数据量过大,例如一次从数据库中取出过多的数据。或者集合类中对于对象的引用在使用完毕后没有清空,使得JVM不能回收。或者代码中存在死循环或过多重复的对象实体。启动参数内存值设定过小等情况都可能存在Java内存溢出。
    (10)类加载机制
    首先,类加载其是将class文件加载到虚拟机内存中区,通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个class对象。然后将对象进行链接,执行校验、准备、解析的过程。校验主要有四种方式:文件格式验证、元数据验证、字节码验证、符号引用验证。准备阶段则是给类的静态变量分配内存,并赋予默认值(不包含用final修饰的static)。解析则是将常量池中的符号引用替换为直接引用的过程。包含类、接口、字段、类方法、接口方法的解析。然后对类的静态变量初始化指定值,执行静态代码块。

    相关文章

      网友评论

          本文标题:Java编程思想总结

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