Search code examples
javaobjectfloating-pointjvmprogress-bar

Why does a primitive variable change value without being updated? [Java]


I created a small class to display a progress bar in the terminal. It is multi-threaded to avoid tampering with the performance of the calling process. I did not want the loop that prints the bar to print it constantly as this causes flickering because the terminal updates do not always line up with the printing, which causes flickering.

So here is where the problem comes in: I added a variable called lastProgress. It is initialized to 0 and from there on out only updates after the progress bar is printed. "progress" is the variable that keeps track of the progress of the bar. step() increases progress. Thus if progress increases it will be bigger than lastProgress and the bar will print, then lastProgress will be set equal to progress. This means that the bar won't be printed again until progress increases (by a call to step()).

However, this code does not do this. It prints the bar ONCE and then never again. A trace of the values of progress and lastProgress shows that every time progress increases, lastProgress also increases. But the only place in the code that changes the value of lastProgress (which is at the end of the run method) is NEVER EXECUTED. A print statement just before it only ever executes once, yet the value keeps changing. How does the value of lastProgress keep updating if the code that updates it is never ran??

I even decompiled the code with javap to see what the bytecode does. Maybe the first time lastProgress = progress is ran it copies by reference? This is not the case, the bytecode confirmed it. It should not be as primitives are copied by value by default in Java as far as I am aware.

I am losing my mind. Please help me.

Here is my code:

package utils;

public class ProgressBar implements Runnable {
  private String message;
  private final float maxProgress;
  private float progress;
  private float lastProgress;
  private boolean running;
  private final int PROGRESS_BAR_LENGTH = 50;

  public ProgressBar(String message, int maxProgress) {
    this.maxProgress = maxProgress;
    this.message = message;
    progress = 0;
    lastProgress = 0;
    running = true;
  }

  @Override
  public void run() {
    StringBuilder s;

    while (running) {
      // The check for the print happens here
      if (progress > lastProgress) {
        s = new StringBuilder();
        float progressLevel = (progress / maxProgress) * PROGRESS_BAR_LENGTH;
        s.append("┃");
        for (int i = 1; i <= PROGRESS_BAR_LENGTH; i++) {
          if (i <= progressLevel) s.append("█");
          else s.append(" ");
        }
        s.append("┃");

        System.out.printf(
            "%s:    %s    %.0f%% \r", message, s, progressLevel * (100 / PROGRESS_BAR_LENGTH));

        // THIS IS THE ONLY PLACE LASTPROGRESS GETS UPDATED
        lastProgress = progress;
      }

    // This print is executed to ensure that the final update is shown
    s = new StringBuilder();
    float progressLevel = (progress / maxProgress) * PROGRESS_BAR_LENGTH;
    s.append("┃");
    for (int i = 1; i <= PROGRESS_BAR_LENGTH; i++) {
      if (i <= progressLevel) s.append("█");
      else s.append(" ");
    }
    s.append("┃");

    System.out.printf(
        "%s:    %s    %.0f%% \r", message, s, progressLevel * (100 / PROGRESS_BAR_LENGTH));
  }

  public void step() {
    progress++;
    if (progress >= maxProgress) {
      progress = maxProgress;
      running = false;
    }
  }

  public void step(float step) {
    progress += step;
    if (progress >= maxProgress) {
      progress = maxProgress;
      running = false;
    }
  }

  public void setMessage(String newMessage) {
    message = newMessage;
  }

  public void stepTo(int newProgress) {
    progress = newProgress;
    if (progress > maxProgress) {
      progress = maxProgress;
      running = false;
    }
  }

  public static void main(String[] args) throws InterruptedException {
    ProgressBar pb = new ProgressBar("Test", 100);
    new Thread(pb).start();

    for (int i = 0; i < 100; i++) {
      pb.step();
      Thread.sleep(2000);
    }
  }
}

It no work :( I asked my friend and it baffled him too

Edit: I want to clarify that I misused the term "multi-threaded" as I just intended for the program to run on its own thread to avoid a performance hit on the calling thread. I also do not need thread safety as only one instance of this program will ever be run at a time and it will manage its own state internally.


Solution

  • Here you go, It works for me at least. (tested on macos)

    package main.java;
    
    public class ProgressBar implements Runnable {
    private String message;
    private final float maxProgress;
    private float progress;
    private float lastProgress;
    private boolean running;
    private final int PROGRESS_BAR_LENGTH = 50;
    
    public ProgressBar(String message, int maxProgress) {
        this.maxProgress = maxProgress;
        this.message = message;
        progress = 0;
        lastProgress = 0;
        running = true;
    }
    
    @Override
    public void run() {
        StringBuilder s = new StringBuilder();
    
        float progressLevel = 0;
        while (running) {
            // The check for the print happens here
            if (progress > lastProgress) {
                s = new StringBuilder();
                progressLevel = (progress / maxProgress) * 
    PROGRESS_BAR_LENGTH;
                s.append("┃");
                for (int i = 1; i <= PROGRESS_BAR_LENGTH; i++) {
                    if (i <= progressLevel) s.append("█");
                    else s.append(" ");
                }
                s.append("┃");
    
                System.out.printf(
                        "%s:    %s    %.0f%% \r", message, s, progressLevel * (100 / PROGRESS_BAR_LENGTH));
    
                // THIS IS THE ONLY PLACE LASTPROGRESS GETS UPDATED
                lastProgress = progress;
            }
    
    
            s = new StringBuilder();
            progressLevel = (progress / maxProgress) * PROGRESS_BAR_LENGTH;
            s.append("┃");
            for (int i = 1; i <= PROGRESS_BAR_LENGTH; i++) {
                if (i <= progressLevel) s.append("█");
                else s.append(" ");
            }
            s.append("┃");
        }
    
    
        System.out.printf(
                "%s:    %s    %.0f%% \r", message, s, progressLevel * (100 / PROGRESS_BAR_LENGTH));
    }
    
    public void step() {
        progress++;
        if (progress >= maxProgress) {
            progress = maxProgress;
            running = false;
        }
    }
    
    public void step(float step) {
        progress += step;
        if (progress >= maxProgress) {
            progress = maxProgress;
            running = false;
        }
    }
    
    public void setMessage(String newMessage) {
        message = newMessage;
    }
    
    public void stepTo(int newProgress) {
        progress = newProgress;
        if (progress > maxProgress) {
            progress = maxProgress;
            running = false;
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        ProgressBar pb = new ProgressBar("Test", 100);
        new Thread(pb).start();
    
        for (int i = 0; i < 100; i++) {
            pb.step();
            Thread.sleep(1000);
        }
    }
    }
    

    enter image description here

    Edit. What I changed.

    1. I added one closing bracket as code didnt compile before.
    2. I moved second printf outside a while loop. With it inside the console was flickering when updating values
    3. I moved initialization of string builder and progress level outside a while loop.