Java基础之代码死循环详解

一、前言

代码死循环这个话题,个人觉得还是挺有趣的。因为只要是开发人员,必定会踩过这个坑。如果真的没踩过,只能说明你代码写少了,或者是真正的大神。

尽管很多时候,我们在极力避免这类问题的发生,但很多时候,死循环却悄咪咪的来了,坑你于无形之中。我敢保证,如果你读完这篇文章,一定会对代码死循环有一些新的认识,学到一些非常实用的经验,少走一些弯路。

二、死循环的危害

我们先来一起了解一下,代码死循环到底有哪些危害?

  • 程序进入假死状态, 当某个请求导致的死循环,该请求将会在很大的一段时间内,都无法获取接口的返回,程序好像进入假死状态一样。
  • cpu使用率飙升,代码出现死循环后,由于没有休眠,一直不断抢占cpu资源,导致cpu长时间处于繁忙状态,必定会使cpu使用率飙升。
  • 内存使用率飙升,如果代码出现死循环时,循环体内有大量创建对象的逻辑,垃圾回收器无法及时回收,会导致内存使用率飙升。同时,如果垃圾回收器频繁回收对象,也会造成cpu使用率飙升。
  • StackOverflowError,在一些递归调用的场景,如果出现死循环,多次循环后,最终会报StackOverflowError栈溢出,程序直接挂掉。

三、哪些场景会产生死循环?

3.1 一般循环遍历

这里说的一般循环遍历主要是指:

  • for语句
  • foreach语句
  • while语句

这三种循环语句可能是我们平常使用最多的循环语句了,但是如果没有用好,也是最容易出现死循环的问题的地方。让我们一起看看,哪些情况会出现死循环。

3.1.1 条件恒等

很多时候我们使用for语句循环遍历,不满足指定条件,程序会自动退出循环,比如:

for(int i=0; i<10; i++) {
   System.out.println(i);
}

但是,如果不小心把条件写错了,变成这样的:

for(int i=0; i>=0; i++) {
   System.out.println(i);
}

结果就悲剧了,必定会出现死循环,因为循环中的条件变成恒等的了。

很多朋友看到这里,心想这种错误我肯定不会犯的。不过我需要特别说明的是,这里举的例子相对来说比较简单,如果i>=0这里是个非常复杂的计算,还真说不准一定不会出现死循环。

3.1.2 不正确的continue

for语句在循环遍历数组list时更方便,而while语句的使用场景却更多。

有时候,在使用while语句遍历数据时,如果遇到特别的条件,可以使用continue关键字跳过本次循环,直接执行下次循环。

例如:

int count = 0;
while(count < 10) {
   count++;
   if(count == 4) {
      continue;
   }
   System.out.println(count);
}

当count等于4时,不打印count。

但如果continue没有被正确使用,可能会出现莫名奇怪的问题:

int count = 0;
while(count < 10) {
   if(count == 4) {
      continue;
   }
   System.out.println(count);
   count++;
}

当count等于4时直接推出本次循环,count没有加1,而直接进入下次循环,下次循环时count依然等4,最后无限循环了。

这种是我们要千万小心的场景,说不定,已经进入了死循环你还不知道呢。

3.1.3 flag线程间不可见

有时候我们的代码需要一直做某件事情,直到某个条件达到,有个状态告诉它,要终止任务了,它就会自动退出。

这时候,很多人都会想到用while(flag)实现这个功能:

public class FlagTest {
    private boolean flag = true;

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    public void fun() {
        while (flag) {
        }
        System.out.println("done");
    }

    public static void main(String[] args) throws InterruptedException {
        final FlagTest flagTest = new FlagTest();
        new Thread(() -> flagTest.fun()).start();
        Thread.sleep(200);
        flagTest.setFlag(false);
    }
}

这段代码在子线程中执行无限循环,当主线程休眠200毫秒后,将flag变成false,这时子线程就会自动退出了。想法是好的,但是实际上这段代码进入了死循环,不会因为flag变成false而自动退出。

为什么会这样?

线程间flag是不可见的,这时如果flag加上了volatile关键字,变成:

private volatile boolean flag = true;

会强制把共享内存中的值刷新到主内存中,让多个线程间可见,程序可以正常退出。

3.2 Iterator遍历

除了前面介绍过的一般循环遍历之外,遍历集合的元素,还可以使用Iterator遍历。当然并非所有集合都能使用Iterator遍历,只有实现了Iterator接口的集合,或者该集合的内部类实现了Iterator接口才可以。

例如:

public class IteratorTest {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("123");
        list.add("456");
        list.add("789");

        Iterator<String> iterator = list.iterator();
        while(iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

但如果程序改成这样:

public class IteratorTest {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("123");
        list.add("456");
        list.add("789");

        while(list.iterator().hasNext()) {
            System.out.println(list.iterator().next());
        }
    }
}

就会出现死循环。

这是什么呢?

如果看过ArrayList源码的朋友,会发现它的底层iterator方法是这样的实现的:

public Iterator<E> iterator() {
    return new Itr();
}

每次都new了一个新的Itr对象。而hasNext方法的底层是通过判断游标和元素个数是否相等实现的:

public boolean hasNext() {
    return cursor != size;
}

每次new了一个新的Itr对象的时候cursor值是默认值0,肯定和元素个数不相等。所以导致while语句中的条件一直都成立,所以才会出现死循环。

我们都需要注意:在while循环中使用list.iterator().hasNext(),是个非常大的坑,千万小心。

3.3 类中使用自己的对象

在某个类中把自己的对象定义成成员变量,不知道你有没有这样做过。

有些可能会很诧异,为什么要这么做。

假如,你需要在一个方法中调用另一个打了@Transactional注解的方法,这时如果直接方法调用,另外一个方法由于无法走代理事务会失效。比如:

@Service
public class ServiceA {

   public void save(User user) {
         System.out.println("业务处理");
         doSave(user);
   }

   @Transactional(rollbackFor=Exception.class)
   public void doSave(User user) {
       System.out.println("保存数据");
    }
 }

这种场景事务会失效。

这时可以通过把该类自己定义成一个成员变量,通过该变量调用doSave方法就能有效的避免该问题。

@Service
public class ServiceA {
   @Autowired
   private ServiceA serviceA;

   public void save(User user) {
         System.out.println("业务处理");
         serviceA.doSave(user);
   }

   @Transactional(rollbackFor=Exception.class)
   public void doSave(User user) {
       System.out.println("保存数据");
    }
 }

当然还有其他办法解决这个问题,不过这种方法是最简单的。

问题来了,如果成员变量不是通过@Autowired注入,而是直接new出来的,可以吗?

成员变量改成这样之后:

private ServiceA serviceA = new ServiceA();

项目在启动的时候,程序进入无限循环,不断创建ServiceA对象,但一直都无法成功。最后会报java.lang.StackOverflowError栈溢出,当栈深度超过虚拟机分配给线程的栈大小时就会出现此错误。

为什么会出现这个问题?

因为程序在实例化ServiceA对象时,要先实例化它的成员变量serviceA,但是它的成员变量serviceA,又需要实例化它自己的成员变量serviceA,如此一层层实例化下去,最终也没能实例化。

@Autowired注入为什么没有问题?

因为@Autowired是在ServiceA对象实例化成功之外,在依赖注入阶段,把实例注入到成员变量serviceA的。在spring中使用了三级缓存,通过提前暴露ObjectFactory对象来解决这个自己依赖自己的循环依赖问题。

对spring循环依赖问题有兴趣的朋友,可以看看我之前写的一篇文章《》。

3.4 无限递归

在日常工作中,我们需要经常使用树形结构展示数据,比如:分类、地区、组织、菜单等功能。

很多时候需要从根节点遍历找到所有叶子节点,也需要从叶子节点,往上一直追溯到根节点。

我们以通过根节点遍历找到所有叶子节点为例。由于每次需要一层层遍历查找,而且调用的方法基本相同。为了简化代码,我们一般都会选择使用递归来实现这个功能。

这里我们以根据叶子节点找到根节点为例,大致代码如下:

public Category findRoot(Long categoryId) {
    Category category = categoryMapper.findCategoryById(categoryId);
    if(null == category) {
       throw new BusinessException("分类不存在");
    }
    Long parentId = category.getParentId();
    if(null == categoryId || 0 == categoryId) {
       return category;
    }
    return findRoot(parentId);
}

根据categoryId往上递归查找,如果发现parentId为null或者0的时候,就是根节点了,这时直接返回。

这可能是最普通不过的递归调用了,但是如果有人使坏,或者由于数据库误操作,把根节点的parentId改成了二级分类的categoryId一样,比如都改成:1222。这样递归调用会进入无限循环,最终会报java.lang.StackOverflowError异常。

为了避免这种惨案的发生,其实是有办法的。

可以定义一个运行递归的最大层级MAX_LEVEL,达到了最大层级则直接退出。以上代码可以做如下调整:

private static final int MAX_LEVEL = 6;

public Category findRoot(Long categoryId, int level) {
    if(level >= MAX_LEVEL) {
       return null;
    }
    Category category = categoryMapper.findCategoryById(categoryId);
    if(null == category) {
       throw new BusinessException("分类不存在");
    }
    Long parentId = category.getParentId();
    if(null == categoryId || 0 == categoryId) {
       return category;
    }
    return findRoot(parentId, ++level);
}

先定义MAX_LEVEL的值,然后第一次调用递归方法的时候level字段的值传1,每递归一次level的值加1,当发现level的值大于等于MAX_LEVEL时,说明出现了异常情况,则直接返回null。

我们在写递归方法的时候,要养成好习惯,最好定义一个最大递归层级MAX_LEVEL,防止由于代码bug,或者数据异常,导致出现无限递归的情况。

3.5 hashmap

我们在写代码时,为了提高效率,使用集合的概率非常大。通常情况下,我们喜欢先把数据收集到集合当中,然后对数据进行批处理,比如批量insert或update,提升数据库操作的性能。

我们使用比较多的集合有:ArrayList、HashSet、HashMap等。我个人非常喜欢使用HashMap,特别是在java8中需要嵌套循环的地方,将其中一层循环的数据(list或者set)转换成HashMap,可以减少一层遍历,提升代码的执行效率。

但是如果HashMap使用不当,可能会出现死循环,怎么回事呢?

3.5.1 jdk1.7的HashMap

jdk1.7的HashMap中采用 数组 + 链表 的结构存储数据。在多线程环境下,同时往HaspMap中put数据时,会触发resize方法中的transfer方法,进行数据重新分配的过程,需要重新组织链表的数据。

由于采用了头插法,最终会形成key3的next等于key7,而key7的next又等于key3的情况,从而构成了死循环。

3.5.2 jdk1.8的HashMap

有了解决jdk1.7扩容时出现死循环的问题,在jdk1.8中对HashMap进行了优化,将jdk1.7中的头插法改成了尾插法,另外采用 数组 + 链表 + 红黑树 的结构存储数据。如果链表中元素超过8个时,就将链表转化为红黑树,以减少查询的复杂度,将时间复杂度降低为O(logN)。

在多线程环境下,同时往HaspMap中put数据时,会触发root方法重新组织树形结构的数据。

在for循环中会出现两个TreeNode节点的Parent引用都是对方,从而构成死循环的情况。

3.5.3 ConcurrentHashMap

由于在多线程环境下,使用无论是jdk1.7,还是jdk1.8的HashMap会有死循环的问题。所以很多人建议,不用在多线程环境下,使用HashMap,而应该改用ConcurrentHashMap

ConcurrentHashMap是线程安全的,同样采用了 数组 + 链表 + 红黑树 的结构存储数据,此外还是使用了 cas + 分段锁,默认是16段锁,保证并发写入时,数据不会产生错误。

在多线程环境下,同时往ConcurrentHashMapcomputeIfAbsent数据时,如果里面还有一个computeIfAbsent,它们的key对应的hashCode是一样的,这时就会产生死循环。

意不意外,惊不惊喜?

幸好这个bug在jdk1.9中已经被Doug Lea修复了。

3.6 动态代理

我们在实际工作中,即使没有自己动手写过动态代理程序,但也听过或者接触过,因为很多优秀的开发框架,它们的底层必定都会使用动态代理,实现一些附加的功能。通常情况下,我们使用最多的动态代理是:JDK动态代理Cglib,spring的AOP就是通过这两种动态代理技术实现的。

我们在这里以JDK动态代理为例:

public interface IUser {
    String add();
}
public class User implements IUser {
    @Override
    public String add() {
        System.out.println("===add===");
        return "success";
    }
}
public class JdkProxy implements InvocationHandler {

    private Object target;

    public Object getProxy(Object target) {
        this.target = target;
        return Proxy.newProxyInstance(this.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(target,args);
        after();
        return result;
    }

    private void before() {
        System.out.println("===before===");
    }

    private void after() {
        System.out.println("===after===");
    }
}
public class Test {
    public static void main(String[] args) {
        User user = new User();
        JdkProxy jdkProxy = new JdkProxy();
        IUser proxy = (IUser)jdkProxy.getProxy(user);
        proxy.add();
    }
}

实现起来主要有三步:

1.实现某个具体业务接口

2.InvocationHandler接口,创建调用关系

3.使用Proxy创建代理类,指定被代理类的相关信息

这样在调用proxy的add方式时,会自动调用before和after方法,实现了动态代理的效果,是不是很酷?

通常情况下,这种写法是没有问题的,但是如果在invoke方法中调用了proxy对象的toString方法,加了这段代码:

proxy.toString();

程序再次运行,循环很多次之后,就会报java.lang.StackOverflowError异常。

很多人看到这里可能一脸懵逼,到底发生了什么?

代理对象本身并没有自己的方法,它的所有方法都是基于被代理对象的。通常情况下,如果访问代理对象的方法,会经过拦截器的invoke方法。但是如果在invoke方法调了代理对象的方法,比如:toString方法,会经过另外一个拦截器的invoke方法,如此一直反复调用,最终形成死循环。

切记不要在invoke方法中调用代理对象的方法,不然会产生死循环,坑你于无形之中。

3.7 我们自己写的死循环

很多朋友看到这个标题,可能会质疑,我们自己会写死循环?

没错,有些场景我们还真的会写。

3.7.1 定时任务

不知道你有没有手写过定时任务,反正我写过,是非常简单的那种(当然复杂的也写过,在这里就不讨论了)。如果有个需求要求每隔5分钟,从远程下载某个文件最新的版本,覆盖当前文件。

这时候,如果你不想用其他的定时任务框架,可以实现一个简单的定时任务,具体代码如下:

public static void downLoad() {
    new Thread(() -> {
        while (true) {
            try {
                System.out.println("download file");
                Thread.sleep(1000 * 60 * 5);
            } catch (Exception e) {
                log.error(e);
            }
        }
    }).start();
}

其实很多JDK中的定时任务,比如:Timer类的底层,也是用了while(true)的无限循环(也就是死循环)来实现的。

3.7.2 生产者消费者

不知道你有没有手写过生产者和消费者。假设有个需求需要把用户操作日志写入表中,但此时消费中还没有引入消息中间件,比如:kafka等。

最常规的做法是在接口中同步把日志写入表中,保存逻辑跟业务逻辑可能在同一个事务中,但为了性能考虑,避免大事务的产生,一般建议不放在同一个事务。

原本挺好的,但是如果接口并发量上来了,为了优化接口性能,可能会把同步写日志到表中的逻辑,拆分出来,做成异步处理的。

这时候,就可以手动撸一个生产者消费者解决这个问题了。

@Data
public class Car {
    private Integer id;
    private String name;
}
@Slf4j
public class Producer implements Runnable {

    private final ArrayBlockingQueue<Car> queue;

    public Producer(ArrayBlockingQueue<Car> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        int i = 1;
        while (true) {
            try {
                Car car = new Car();
                car.setId(i);
                car.setName("汽车" + i);
                queue.put(car);
                System.out.println("Producer:" + car + ", queueSize:" + queue.size());
            } catch (InterruptedException e) {
                log.error(e.getMessage(),e);
            }
            i++;
        }
    }
}
@Slf4j
public class Consumer implements Runnable {

    private final ArrayBlockingQueue<Car> queue;

    public Consumer(ArrayBlockingQueue<Car> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Car car = queue.take();
                System.out.println("Consumer:" + car + ",queueSize:" + queue.size());
            } catch (InterruptedException e) {
                log.error(e.getMessage(), e);
            }
        }
    }
}
public class ClientTest {

    public static void main(String[] args) {
        ArrayBlockingQueue<Car> queue = new ArrayBlockingQueue<Car>(20);
        new Thread(new Producer(queue)).start();
        new Thread(new Producer(queue)).start();
        new Thread(new Consumer(queue)).start();
    }
}

由于ArrayBlockingQueue阻塞队列内部通过notEmptynotFull 这两个Condition实现了阻塞和唤醒机制,所以我们无需再做额外控制,用它实现生产者消费者相对来说要容易多了。

四、自己写的死循环要注意什么?

不知道聪明的小伙伴们有没有发现,我们自定义的定时任务生产者消费者例子中,也写了死循环,但跟上面其他的例子都不一样,我们写的死循环没有出现问题,这是为什么?

定时任务中我们用了sleep方法做休眠:Thread.sleep(300000);

生产者消费者用了Condition类的awaitsignal方法实现了阻塞和唤醒机制。

这两种机制说白了,都会主动让出cpu一段时间,让其他的线程有机会使用cpu资源。这样cpu有上下文切换的过程,有一段时间是处于空闲状态的,不会像其他的列子中一直处于繁忙状态。

一直处于繁忙状态才是cpu使用率飙高的真正原因,我们要避免这种情况的产生。

就像我们平时骑共享单车(cpu资源)一样,我们一般骑1-2小时就会归还了,这样其他人就有机会使用这辆共享单车。但如果有个人,骑了一个天还没归还,那么这一天当中自行车一直处于繁忙之中,其他人就没有机会骑这辆自行车了。

到此这篇关于Java基础之代码死循环详解的文章就介绍到这了,更多相关Java代码死循环内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java非静态成员变量之死循环(详解)

    1.非静态成员变量 当成员变量为非静态成员变量且对当前类进行实例化时,将会产生死循环 例子: public class ConstructorCls { private ConstructorCls obj=new ConstructorCls(); } public class TestC { public static void main(String[] args) { ConstructorCls c =new ConstructorCls(); } } 结果: Exception in

  • Java中一个线程执行死循环有什么后果

    假设有两个线程在并发运行,一个线程执行的代码中含有一个死循环如:while(true)....当该线程在执行while(true)中代码时,另一个线程会有机会执行吗? 示例代码(代码来源于互联网) public class Service { Object object1 = new Object(); public void methodA() { synchronized (object1) { System.out.println("methodA begin"); boolea

  • java排查一个线上死循环cpu暴涨的过程分析

    问题,打一个页面cpu暴涨,打开一次就涨100%,一会系统就卡的不行了. 排查方法,因为是线上的linux,没有用jvm监控工具rim链接上去. 只好用命令排查: top cpu排序,一个java进程cpu到500%了,什么鬼..... 查到对应java进程 jps || ps -aux | grep 端口 pid=13455 查看进程中线程使用情况 T排序 查看cpu占用time最高的线程编号 top -Hp 13455 有个线程9877 的时间一直在爆涨 获取线程十六进制地址9877 (十六

  • Java中一个for语句导致无穷大死循环的例子

    在Java开发中常用到For循环,它对简化业务处理,提高效率,非常有帮助.但要防止程序算法中可能导致死循环的情况,而且有的死循环还不好察觉.比如下面这个例子,算法极容易认为是50,实际上是无穷大的一个死循环. public class CycTest { /** * @param args the command line arguments */ public static void main(String[] args) { int end = Integer.MAX_VALUE; //定义

  • 基于Java HashMap的死循环的启示详解

    一.单线程改造为多线程也是个技术活 正如我们看到耗子叔叔博客里写的那样,原来是单线程的应用程序,"后来,我们的程序性能有问题,所以需要变成多线程的,于是,变成多线程后到了线上,发现程序经常占了100%的CPU". 考虑到是淘宝的工程师曝出来的问题,他们的技术基础一般都很扎实,连他们都用错了,所以把单线程改造为多线程并不是想象中的那么简单,我认为. 你可能很不服气地反问,淘宝的工程师又怎么了,单线程改为多线程有什么难的?无非就是应用现有的多线程技术嘛,你看,我有非常强烈的线程安全意识,我

  • Java并发之不可思议的死循环详解

    下面的代码将发生死循环: package com.zzj.concurrency; public class VolatileObjectTest implements Runnable{ private ObjectA objectA; // 加上volatile 就可以正常结束While循环了 public VolatileObjectTest(ObjectA a) { this.objectA = a; } public ObjectA getA() { return objectA; }

  • 深入了解JAVA HASHMAP的死循环

    前言 在淘宝内网里看到同事发了贴说了一个CPU被100%的线上故障,并且这个事发生了很多次,原因是在Java语言在并发情况下使用HashMap造成Race Condition,从而导致死循环.这个事情我4.5年前也经历过,本来觉得没什么好写的,因为Java的HashMap是非线程安全的,所以在并发下必然出现问题.但是,我发现近几年,很多人都经历过这个事(在网上查"HashMap Infinite Loop"可以看到很多人都在说这个事)所以,觉得这个是个普遍问题,需要写篇疫苗文章说一下这

  • Java基础之代码死循环详解

    一.前言 代码死循环这个话题,个人觉得还是挺有趣的.因为只要是开发人员,必定会踩过这个坑.如果真的没踩过,只能说明你代码写少了,或者是真正的大神. 尽管很多时候,我们在极力避免这类问题的发生,但很多时候,死循环却悄咪咪的来了,坑你于无形之中.我敢保证,如果你读完这篇文章,一定会对代码死循环有一些新的认识,学到一些非常实用的经验,少走一些弯路. 二.死循环的危害 我们先来一起了解一下,代码死循环到底有哪些危害? 程序进入假死状态, 当某个请求导致的死循环,该请求将会在很大的一段时间内,都无法获取接

  • Java基础之关键字final详解

    Java-关键字:final 1 .final可以用来修饰的结构: 类.方法.变量 2.final 用来修饰一个类: 此类不能被其他类所继承 比如:String类.System类.StringBuffer类 3.final 用来修饰方法: 表明此方法不可以被重写 比如:Object类中getClass(); 4.final 用来修饰变量,此时的"变量"就称为是一个常量 4.1 final修饰属性: 可以考虑赋值的位置有:显示初始化.代码块中初始化.构造器中初始化 4.2 final修饰

  • Java基础之Object类详解

    object类的介绍 object是所有类的直接父类或者是间接父类,为什么这么说呢? 可以查询java8的API帮助文档: 可见在这样的一个类树中,所有的类的根还是Object类 在IDEA中新建一个类,系统会默认继承Object类 public class Pet extends Object{ } 那么Dog继承了Pet类的属性和行为方法,还会继承Object类的属性和行为方法了吗?这一点是肯定的,Pet类作为Object类的子类,Dog类作为Pet类的子类,所以说Object是Dog类的间

  • Java基础之容器Vector详解

    一.前言 知识补充:Arrays.copyOf函数: public static int[] copyOf(int[] original, int newLength) { int[] copy = new int[newLength]; System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; } 可见copyOf()在内部新建一个数组,调用arrayCopy()将ori

  • java基础开发泛型类的详解

    目录 前言 泛型概念 泛型类 结论 前言 在软件开发中,有许多执行过程很类似,许多人使用复制粘贴完成功能,这种做法虽然编译器不会报错,但会使用波浪线给出提示,给以后的维护带来了很大的隐患.这种情况开发人员通常根据需要成员抽取公用方法.公用类或使用继承完成,提高了代码的复用.但是,在一些特殊情况(如执行过程中会使用到对象,这些对象操作相同,但具体的模块有有所区别),此时只能使用泛型完成代码的复用. 泛型概念 所谓泛型就是将类型由原来的具体类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式

  • java基础之注解示例详解

    目录 定义 作用 注解与注释的区别 JDK内置的标准注解 自定义注解 @Target 属性 定义 注解也叫原数据,它是JDK1.5及之后版本引入的一个特性,它可以声明在类.方法.变量等前面,用来对这些元素进行说明. 作用 生成文档:通过代码里标识的注解生成doc文档[生成doc文档] 代码分析:通过代码里标识的注解对代码进行分析[反射] 编译检查:通过代码里标识的注解让编译器能够实现基本的编译检查[Override] 注解与注释的区别 注解是给编译器看的,注释是给程序员看的. JDK内置的标准注

  • Java基础学习之构造方法详解

    目录 一.构造方法概述 二.构造方法的注意事项 三.标准类制作 一.构造方法概述 构造方法是一种特殊的方法 作用:创建对象Student stu = new Student(); 格式: pucli class 类名{        修饰符 类名(参数){        } } 功能:主要是完成对象数据的初始化 示例代码: class Student { private String name; private int age; //构造方法 public Student() { System.

  • Java基础学习之接口详解

    目录 概述 定义格式 含有抽象方法 含有默认方法和静态方法 含有私有方法和私有静态方法 基本的实现 实现的概述 抽象方法的使用 默认方法的使用 静态方法的使用 私有方法的使用 接口的多实现 抽象方法 默认方法 静态方法 优先级的问题 接口的多继承 其他成员特点 概述 接口,是Java语言中一种引用类型,是方法的集合,如果说类的内部封装了成员变量.构造方法和成员方法,那么接口的内部主要就是封装了方法,包含抽象方法(JDK 7及以前),默认方法和静态方法(JDK 8),私有方法 (JDK 9). 接

  • Java基础之集合框架详解

    一.前言 本节学习到的内容有以下5类,不分先后顺序: 集合Collection体系结构 List子类 与集合结合使用的迭代器对象 集合与数组的区别? 常见的一般数据结构整理 二.集合的由来? Collection List ArrayList Vector LinkedList Set hashSet treeSet 在集合没有出现之前,使用对象数组来存储对象,但是,对象数组的长度一旦确定,则不可以发生变化,所以我们希望存在一个容器就像StringBuffer一样存储字符串,同时依据传入的值的个

随机推荐