A blog about programming topics in general, focusing on the Java programming language.

Month: February 2023

How to ignore files in Git

Introduction

Nowadays, every developer out there must be familiar with some version control system. I have now worked in 5 different jobs. Every single one of them was using some kind of version control system.

Even though there are many of them, the most used by far is Git.

When working on a real project, we often need to create or modify a file we don’t want to be added to the repository.

The most common scenario for a backend developer is probably some local file configuration to access to a local database or some kind of parameters that need to be passed to the execution of the server itself.

For a frontend developer, you may be sick of hearing this but in case you forgot it, I’ll remind you: Never upload the node-modules folder! The node-modules folder is a folder that contains all dependencies of a project. It is usually generated over a npm install command that installs all dependencies for a given project (shoutout to all of us who someday did upload a node-modules folder 😅). This is equivalent to the folder of a Java project using Maven that contains all the dependencies. Imagine uploading that to your Git repository.

.Gitignore file

The most used approach is probably the .gitignore file. It is a file that is located in every local git project. As you can imagine, and given that the file starts with a dot, it is a hidden file.

Now, it may vary its exact location depending on which IDE you’re using. I know for a fact that if you are using IntelliJ Idea it is located inside the .idea folder.

Its content will vary but this just so you have an idea of what it looks like, this is the default one for a generated Java project using IntelliJ Idea:

# Default ignored files
/shelf/
/workspace.xml

In some other IDEs such as Visual Studio it is usually placed in the same project’s main path. If we use npm we will probably have a structure like this one.

# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*

As you can see, the /node_modules folder is already added by default to the .gitignore file.

How do we make a file not eligible to be added to our git staging area? Well, we can just add its path to this .gitignore file.

The pros of using this approach are quite evident. We have a shared file with all the developers in our team, because this file is uploaded to the git repository.

If we, as a team, want to ignore some files or folders for all of us, the developers, all we have to do is to add those in the .gitignore file.

Precisely, because it’s a shared file, what if we just want to ignore a private file that won’t be uploaded to the repo? We can’t do it by adding it to our .gitignore file.

But fear no more! Because in those cases we can make use of the following approach.

Global exclude file

There is one file, which path is .git/info/excludes that its purpose is to ignore private files. This configuration won’t be pushed to the repository, therefore it’s a private configuration.

The idea behind this is pretty much the same than the .gitignore file but it’s worth to notice that this file won’t be uploaded to the remote repository.

This approach allows us to untrack some files without sharing this configuration with our team. This is incredibly beneficial for local configuration files, for instance.

Example

With the following project structure

Project structure

I do have a newly added Main.java file and a local.conf file. But let’s say I only want to add the Main.java file to the remote repository.

Since this local.conf is my personal and private local configuration that won’t be shared with the rest of the developers, I will add it to the .git/info/excludes file as we explained before.

Therefore, I will just open the file in a text editor and add it there:

# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~
local.conf

As you can see I just added local.conf to the file.

But if we run a git status we will find a surprise! It’s still showing as added:

PS D:\Projects\git-testing> git status
On branch feature-foo
Your branch is up to date with 'origin/feature-foo'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   local.conf
        new file:   src/main/java/Main.java

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        .idea/

Have in mind that if the file was previously checked in, we will have to run the following command in order to be removed:

git rm --cached local.conf

Note: Sometimes we will have to use the -f (force) flag.

If we run git status again we will see that our local.conf file has disappeared:

PS D:\Projects\git-testing> git status
On branch feature-foo
Your branch is up to date with 'origin/feature-foo'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   src/main/java/Main.java

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        .idea/

Conclusion

Now that you’ve learned how to ignore files in git, what are you waiting for? Stop wasting your time modifying that local configuration file that we don’t want to push to our repo, add it to the excludes file or talk to your team about adding it to the .gitignore file.

I hope you enjoyed this quick post, do you know some other ways to ignore files in Git? Let me know in the comments!

String interning in Java

Introduction

String interning is a concept that not many Developers out there know. It’s deeply bounded to the way the JVM handles memory.

Nowadays it’s easier than ever to leverage the existence of built-in functions, libraries, frameworks… Don’t get me wrong, they help us stop reinventing the wheel over and over.

However, I also feel like it’s hard to know what’s going on underneath. This may not even be a problem at all unless you have to deal with specific circumstances, such as memory management.

Therefore, it’s quite useful to, at least have, a basic understanding of everything you can. By digging deeper, eventually, not everything will be a black box to you.

In today’s post, we will talk about how Strings work in Java, how the JVM handle them, the best way to treat them, and some useful information. I hope you like it. 😊

How does the JVM handle Strings

String is a special kind of class in Java. It’s the only one that we can instantiate with double quotes. The other classes that can be instantiated without using the new keywords are the primitive types.

What happens when you instantiate a String with double quotes?

Java has what is known as String pool. We can think of it as a bag that contains Strings. Every time we create a String that is not yet in the String pool, the JVM adds it.

As we can see in the example, once the String a is created, “Hello” is added to the String pool. Then a new String b is referenced to a. Remember that using the = operator in Java means that the left side (in this case the String b) will point to the memory address of the right side (the String a). However, in the case of the Strings, b is now pointing to the “Hello” String in the String pool.

When the String c is created, given that is initialized to “World” and it is not yet in the String pool, it gets added there as well.

This way, the JVM optimizes memory allocation and consumption as it will only allocate the space of the “Hello” String once.

A curious thing is that you can use the new operator to create a String as shown in the example. When we create the String d using this new operator, instead of it pointing to the already existing “Hello” String in the pool, it allocates memory for it as it would do for a regular object.

This is why you should not create Strings using the constructor.

Immutability

A really important concept about the String class is that it’s immutable. Now, what does this mean? An immutable object is one that can’t be modified. Therefore, when we want to modify an immutable object, we have to create another one. Once we instantiate an immutable object, we won’t be able to change its value.

This happens with many other classes in Java, such as Date, all the wrapper classes of the primitives’ types: Integer, Double and so many more.

This also means that any operation performed over a String won’t modify the String. It will instead, create a new one. That’s why, when you perform an operation on a String but don’t assign it back, nothing will change in the original String.

String test = "Hello";
test.concat(" World");
System.out.println(test); // "Hello"

test = test.concat(" World");
System.out.println(test); // "Hello World"

What happens under the scenes here is that when the String test is reassigned to the output of test.concat(" World"); a new String is created: "Hello World". The JVM then adds this String This new String to the String pool (if not present yet) and then test will point to this new String in the pool.

Equals vs == operator

These previous explanations come in handy when we think about how should we check that a String is the same as another one.

We all know that the == operator returns true when the memory address of the two objects compared is the same. So, for instance:

int num1 = 5;
int num2 = num1;
System.out.println(num1  == num2); // true

num1 is initialized to 5, the JVM allocates memory for this integer 5 and then makes num1 point to that memory address. When num1 is assigned to num2, both are pointing to the same memory address which means that the == operator will return true.

What happens with Strings then?

String str1 = "Hello";
String str2 = "Hello";
System.out.println(str1 == str2); // true

String str3 = new String("Hello");
String str4 = new String("Hello");
System.out.println(str3 == str4); // false
System.out.println(str1 == str3); // false

String str5 = str3.intern();
System.out.println(str1 == str5); // true

There are a couple of things to explain here.

  • str1 == str2 -> true. As we explained before, both are pointing to the same memory address.
  • str3 == str4 -> false. Due to str3 and str4 are both instantiated with the String constructor, both are pointing to a different memory address.
  • str1 == str3 -> false. One string str1 is in the String pool, the other String str3 is not.
  • str1 == str5 -> true. Quickly explained, the intern() method interns the given String, that is to say, performs String internment in the String pool. Therefore, both are pointing to the same String.

So what do we do? Do we just spin a wheel and accept our fate? Well, actually there’s a better approach, use equals to compare Strings.

The equals method allows us to compare the content of the Strings, rather than the memory address. As a result, the previous example with equals would be:

String str1 = "Hello";
String str2 = "Hello";
System.out.println(str1.equals(str2)); // true

String str3 = new String("Hello");
String str4 = new String("Hello");
System.out.println(str3.equals(str4)); // true
System.out.println(str1.equals(str3)); // true

String str5 = str3.intern();
System.out.println(str1.equals(str5)); // true

No matter what we do, all Strings here have the same content, therefore, the equals method returns true for all of them.

Conclusion

I think I gave you enough reasons to remember that you should always use the equals method to compare Strings and try to avoid the == operator.

Soon enough, I will write a blog post on how equals internally work but until then, feel free to investigate and play around on your own (as the best Developers do). Here are some references to get started though. Such as a guide to the Java String pool or some more examples on String interning.

Hope you found this post useful and enjoyed reading it. If you did, you will find my socials at the bottom of this page. You know what to do next 😉 (much appreciated).

Otherwise, or if you feel like you want to give me your insights on this topic, don’t hesitate to post a comment. I’ll be so happy to help/read your suggestions.

How to squash regular and merge commits

Introduction

Have you ever had to work on some feature branch that took quite a long time to develop? You would have had to get the latest updates from another branch, such as main from time to time.

Maybe for the first updates, just rebasing the main branch onto your feature branch was enough, given that there were not many changes in main yet. This would create a regular commit in your branch and once the development of your feature would be done, you could just squash all your commits to a single one and open a Pull Request to the main branch. Everything looks good, you live to see another day and love your job!

However, if the development takes more time, you might end up in a situation where rebasing main onto your feature branch turns out to be 20 step rebase with multiple files in each step.

As a developer, this is quite painful to deal with because every time you need to sync your branch with main you will spend a lot of time on this.

Fear no more! I will talk about my proposed workaround for this kind of situation:

How to squash regular commits

If you just have regular commits on your branch you can easily squash all of them down to one with the git rebase -i command

In this example, we can see how we have one commit “1” in main and three commits in feature-foo branch: “2“, “3” and "4“.

If we want to squash all commits in feature-foo branch, down to one we would just run the command

$ git rebase -i HEAD~3

that will enter the VIM editor (or the one that you have configured)

Changing pick to squash will (obviously) squash that commit into the next one

We will just have to edit the commit message and there we have our squashed commit

Pro tip: You can actually do this simple operation with your favorite git tool, such as IntelliJ Git window:

Just select all commits you want to squash and click on Squash Commits, edit the commit message and the result will be the very same.

How to squash merge commits

Imagine we have the following situation:

We have been working in the feature-foo branch but we were forced to regularly update it with main work. As a result, we now have several regular commits and 2 merge commits.

We still want to squash all the commits in the feature-foo branch in order to have a single commit before we merge our feature branch to main. However, we can no longer use the git rebase -i command due to the merge commits.

My workaround for this kind of scenario consists of a list of steps:

  • Create a temporal branch from main.
 $ git checkout -b temp main
  • Merge with the --squash flag
$ git merge --squash feature-foo
  • Commit the changes
$ git commit
  • (Optional) Edit the commit message that has just popped up.
  • Move to your feature branch ->
$ git checkout feature-foo
  • Hard reset to the temporary branch
    • TIP: If you wanna test this works before actually pushing to your branch, you can even create a temporary branch from your feature branch and do the git reset there.
$ git reset --hard temp
  • Push the changes
    • Note that we have to specify -f flag because of the hard reset
$ git push -f
  • Now you can remove the temporary branch
$ git branch -d temp

As you can see, our feature-foo branch now contains all the commits squashed into a single one, the 8 commit.

Additional tip: If you have been working on the feature branch for so long, the squashed commit will have the date of the first commit you pushed. That is to say, the one with the older date.

To fix this you can run the command

$ git commit --amend --date="now"

Do you use some other workaround for these situations? Let me know in the comments, I’ll be happy to check them out!