JavaRush /Java Blog /Random EN /Primitive Types in Java: Not So Primitive
Viacheslav
Level 3

Primitive Types in Java: Not So Primitive

Published in the Random EN group

Introduction

Application development can be viewed as working with some data, or rather, their storage and processing. Today I would like to touch on the first key aspect. How is data stored in Java? Here we have two possible formats: a reference and a primitive data type. Let's talk about the kinds of primitive types and the possibilities of working with them (whatever one may say, this is the foundation of our knowledge of a programming language). Java's primitive data types are the foundation upon which everything rests. No, I'm not exaggerating at all. Oracle has a separate Tutorial devoted to primitives: Primitive Data Types Primitive Types in Java: Not So Primitive - 1 A bit of history. At first it was zero. But zero is boring. And then there was a bit(bit). Why was it named like that? They named it so from the abbreviation " bi nary digi t " (binary number). That is, it has only two meanings. And since there was zero, it is logical that now it has become either 0 or 1. And life has become more fun. The beats began to gather in flocks. And these flocks began to be called byte(byte). In the modern world, byte = 2 to the third power, i.e. 8. But it turns out that this was not always the case. There are many guesses, legends and rumors about where the name byte came from. Someone thinks that it's all about the encodings of that time, and someone thinks that it was more profitable to read information this way. A byte is the smallest addressable portion of memory. It is bytes that have unique addresses in memory. There is a legend that ByTe is an abbreviation for Binary Term - a machine word. Machine word - in simple terms, this is the amount of data that the processor can process in one operation. Previously, the size of a machine word was the same as the smallest addressable memory. In Java, variables can only store a byte value. As I said above, there are two kinds of variables in Java:
  • primitive java types store directly the value of data bytes (we will analyze the types of these primitives in more detail below);
  • reference type, stores the bytes of the object's address in Heap, that is, through these variables we get access directly to the object itself (such a remote control from the object)

Java byte

So, history gave us a byte - the minimum amount of memory that we can use. And it consists of 8 bits. The smallest integer data type in java is byte. It is a signed 8-bit type. What does it mean? Let's count. 2^8 would be 256. But what if we want a negative number? And the Java developers decided that the binary code "10000000" would mean -128, that is, the most significant bit (the leftmost bit) would indicate whether the number was negative. The binary "0111 1111" is equal to 127. That is, 128 cannot be designated in any way, because. it will be -128. The full calculation is in this answer: Why is the range of bytes -128 to 127 in Java? To understand how the numbers are obtained, it is worth looking at the picture:
Primitive Types in Java: Not So Primitive - 2
Accordingly, to calculate the size 2 ^ (8-1) = 128. So the minimum border (and it is with a minus) will be -128. And the maximum is 128 - 1 (subtract zero). That is, the maximum will be 127. In fact, we do not work with the byte type very often at a "high level". Basically, this is the processing of "raw" data. For example, when working with data transmission over a network, when the data is a set of 0 and 1 transmitted through some communication channel. Or when reading data from files. They can also be used when working with strings and encodings. Code example:
public static void main(String []args){
        byte value = 2;
        byte shortByteValue = 0b10; // 2
        System.out.println(shortByteValue);
        // Начиная с JDK7 мы можем разделять литералы подчёркиваниями
        byte minByteValue = (byte) 0B1000_0000; // -128
        byte maxByteValue = (byte) 0b0111_1111; // 127
        byte minusByteValue = (byte) 0b1111_1111; // -128 + 127
        System.out.println(minusByteValue);
        System.out.println(minByteValue + " to " + maxByteValue);
}
By the way, do not think that using the byte type will reduce memory consumption. Basically, byte is used to reduce memory consumption when storing data in arrays (for example, storing data received over the network in some buffer, which will be implemented as an array of bytes). But for operations on data, using byte will not meet your expectations. This is due to the Java Virtual Machine (JVM) implementation. Since most systems are 32-bit or 64-bit, byte and short will be converted to 32-bit int, which we'll talk about next. This makes the calculations easier. See Is addition of byte converts to int because of java language rules or because of jvm?. The answer also provides links to the JLS (Java Language Specification). Also, using byte in the wrong place can lead to awkward moments:
public static void main(String []args){
        for (byte i = 1; i <= 200; i++) {
            System.out.println(i);
        }
}
There will be a loop here. Because the value of the counter will reach the maximum (127), an overflow will occur and the value will become -128. And we will never get out of the loop.

short

The limit of values ​​from byte is quite small. Therefore, for the next data type, we decided to double the number of bits. That is, now not 8 bits, but 16. That is, 2 bytes. Values ​​can be calculated in the same way. 2^(16-1) = 2^15 = 32768. So the range is from -32768 to 32767. It is rarely used for any special cases. As the Java language documentation tells us: " you can use a short to save memory in large arrays ".

int

So we got to the most commonly used type. It takes 32 bits, or 4 bytes. In general, we continue to double. The range of values ​​is from -2^31 to 2^31 – 1.

Max int value

The maximum value of int 2147483648 is 1, which is quite a lot. As mentioned above, in order to optimize calculations, since It is more convenient for modern computers, taking into account their bit depth, to read data can be implicitly converted to int. Here is a simple example:
byte a = 1;
byte b = 2;
byte result = a + b;
Such harmless code, and we will get an error: "error: incompatible types: possible lossy conversion from int to byte". Will have to fix to byte result = (byte)(a + b); And another harmless example. What happens if we run the following code?
int value = 4;
System.out.println(8/value);
System.out.println(9/value);
System.out.println(10/value);
System.out.println(11/value);
And we will get the output
2
2
2
2
*sounds of panic*
The thing is that when working with int values, the remainder is discarded, leaving only the integer part (in such cases it is better to use double).

long

We continue to double. Multiply 32 by 2 to get 64 bits. By tradition, this is 4 * 2, that is, 8 bytes. The range of values ​​is from -2^63 to 2^63 - 1. More than enough. This type allows you to count large-large numbers. Often used when dealing with time. Or with long distances, for example. To indicate that the number is long, the literal L - Long is put after the number. Example:
long longValue = 4;
longValue = 1l; // Не ошибка, но плохо читается
longValue = 2L; // Идеально
I want to jump ahead. Further, we will consider the fact that there are corresponding wrappers for primitives, which make it possible to work with primitives as with objects. But there is an interesting feature. Here is an example: On the same Tutorialspoint online compiler, you can check the following code:
public class HelloWorld {

     public static void main(String []args) {
        printLong(4);
     }

    public static void printLong(long longValue) {
        System.out.println(longValue);
    }
}
This code works without errors, everything is fine. But as soon as we change the type from long to Long in the printLong method (that is, the type becomes not primitive, but object), it becomes unclear to Java which parameter we are passing. It starts to assume that an int is being passed and there will be an error. Therefore, in the case of a method, it will be necessary to explicitly specify 4L. Very often long is used as an ID when working with databases.

Java float and Java double

These types are called floating point types. That is, they are not integral types. The float type is 32-bit (like int), and double is called a double-precision type, so it's 64-bit (multiply by 2, whatever we like). Example:
public static void main(String []args){
        // float floatValue = 2.3; lossy conversion from double to float
        float floatValue = 2.3F;
        floatValue = 2.3f;
        double doubleValue = 2.3;
        System.out.println(floatValue);
        double cinema = 7D;
}
And here is an example of a value difference (due to type precision):
public static void main(String []args){
        float piValue = (float)Math.PI;
        double piValueExt = Math.PI;
        System.out.println("Float value: " + piValue );
        System.out.println("Double value: " + piValueExt );
 }
These primitive types are used in mathematics, for example. Here is the proof, a constant for calculating the number PI . Well, in general, you can look at the API of the Math class. Here's something else that should be important and interesting: even the documentation says: “ This data type should never be used for precise values, such as currency. For that, you will need to use the java.math.BigDecimal class instead.Numbers and Strings covers BigDecimal and other useful classes provided by the Java platform. ". That is, money in float and double does not need to be calculated. An example about accuracy using the example of working at NASA: Java BigDecimal, Dealing with high precision calculations Well, to feel it for yourself:
public static void main(String []args){
        float amount = 1.0000005F;
        float avalue = 0.0000004F;
        float result = amount - avalue;
        System.out.println(result);
}
Run this example, and then add 0 before the numbers 5 and 4. And you will see all the horror) There is an interesting report in Russian about float and double in the topic: https://youtu.be/1RCn5ruN1fk Examples of working with BigDecimal can be seen here: Make cents with BigDecimal By the way, float and double can return not only a number. For example, the example below will return Infinity (i.e. infinity):
public static void main(String []args){
        double positive_infinity = 12.0 / 0;
        System.out.println(positive_infinity);
}
And this one will return NAN:
public static void main(String []args){
        double positive_infinity = 12.0 / 0;
        double negative_infinity = -15.0 / 0;
        System.out.println(positive_infinity + negative_infinity);
}
About infinity is clear. What is NaN? It is Not a number , meaning the result cannot be calculated and is not a number. Here's an example: We want to calculate the square root of -4. The square root of 4 is 2. That is, 2 must be squared and then we get 4. And what must be squared to get -4? It won't work, because if there is a positive number, then it will remain. And if it was negative, then a minus by a minus will give a plus. That is, it is not computable.
public static void main(String []args){
        double sqrt = Math.sqrt(-4);
        System.out.println(sqrt + 1);
        if (Double.isNaN(sqrt)) {
           System.out.println("So sad");
        }
        System.out.println(Double.NaN == sqrt);
}
Here's another great overview of floating point numbers: Where is your point?
What else to read:

Java boolean

The next type is boolean (logical type). It can only take the values ​​true or false, which are keywords. Used in logical operations such as while loops, and in branching with if, switch. What can be interesting to know here? Well, for example, theoretically, 1 bit of information is enough for us, 0 or 1, that is, true or false. But in fact, Boolean will take more memory and it will depend on the specific implementation of the JVM. Usually the same amount is spent on this as on int. Alternatively, use BitSet. Here is a brief description from the Java Fundamentals book: BitSet

Java char

So we got to the last primitive type. So, the data in char takes 16 bits and describes the character. In Java, char is encoded in Unicode. The symbol can be set in accordance with two tables (you can see it here ):
  • Table of Unicode characters
  • ASCII character table
Primitive Types in Java: Not So Primitive - 3
Studio example:
public static void main(String[] args) {
    char symbol = '\u0066'; // Unicode
    symbol = 102; // ASCII
    System.out.println(symbol);
}
By the way, char, being in essence all the same a number, supports mathematical operations, such as the sum. And sometimes this can lead to funny consequences:
public class HelloWorld{

    public static void main(String []args){
        String costForPrint = "5$";
        System.out.println("Цена только для вас " +
        + costForPrint.charAt(0) + getCurrencyName(costForPrint.charAt(1)));
    }

    public static String getCurrencyName(char symbol) {
        if (symbol == '$') {
            return " долларов";
        } else {
            throw new UnsupportedOperationException("Not implemented yet");
        }
    }

}
I strongly advise you to check in the online IDE from tutorialspoint . When I saw this puzzle at one of the conferences, it cheered me up. I hope you like the example too) UPDATED: It was at Joker 2017, report: " Java Puzzlers NG S03 - Where are you all climbing from?! ".

Literals

A literal is an explicitly given value. Using literals, you can specify values ​​in different number systems:
  • Decimal system: 10
  • Hexadecimal: 0x1F4, starts at 0x
  • Octal system: 010, starts from zero.
  • Binary (since Java7): 0b101, starts at 0b
On the octal system, I would dwell a little more, because it's funny:
int costInDollars = 08;
This line of code will not compile:
error: integer number too large: 08
Seems like bullshit. And now let's remember about the binary and octal systems. There is no two in the binary system, because there are two values ​​(starting from 0). And the octal system has 8 values, starting from zero. That is, there is no value 8 itself. Therefore, the error, which at first glance seems absurd. And to remember here “after” the rules for translating values:
Primitive Types in Java: Not So Primitive - 4

Wrapper classes

Primitives in Java have their own wrapper classes so that you can work with them as with objects. That is, for each primitive type there is a reference type corresponding to it. Primitive Types in Java: Not So Primitive - 5Wrapper classes are immutable (immutable): this means that after an object has been created, its state - the value of the value field - cannot be changed. Wrapper classes are declared as final: objects, so to speak, are read-only. I would also like to mention that it is impossible to inherit from these classes. Java automatically does conversions between primitive types and their wrappers:
Integer x = 9;          // autoboxing
int n = new Integer(3); // unboxing
The process of converting primitive types to reference types (int-> Integer) is called autoboxing (auto-boxing), and the reverse of it is unboxing (auto-unboxing). These classes make it possible to store a primitive inside an object, and the object itself will behave like an Object (well, like any other object). With all this, we get a large number of motley, useful static methods, such as comparing numbers, converting a character to case, determining whether a character is a letter or a number, finding the minimum number, etc. The provided set of functionality depends only on the wrapper itself. An example of a custom implementation of a wrapper for int:
public class CustomerInt {

   private final int value;

   public CustomerInt(int value) {
       this.value = value;
   }

   public int getValue() {
       return value;
   }
}
The main package, java.lang, already has implementations of the classes Boolean, Byte, Short, Character, Integer, Float, Long, Double, and we don’t need to fence anything of our own, but only reuse the finished one. For example, such classes give us the opportunity to create, say, a List , because a List should only contain objects, which primitives are not. To convert a value of a primitive type, there are static valueOf methods, for example, Integer.valueOf(4) will return an object of type Integer. There are methods intValue(), longValue(), etc. for the reverse conversion. The compiler inserts valueOf and *Value calls on its own, this is the essence of autoboxing and autounboxing. What the autopacking and autopacking example above actually looks like:
Integer x = Integer.valueOf(9);
int n = new Integer(3).intValue();
You can read more about auto-packing and auto-unpacking in this article .

Cast

When working with primitives, there is such a thing as type casting, one of the not-so-nice features of C++, however, type casting is preserved in the Java language. Sometimes we come across situations where we need to interact with data of different types. And it is very good that in some situations it is possible. In the case of reference variables, they have their own characteristics related to polymorphism and inheritance, but today we are considering simple types and, accordingly, casting simple types. There is a widening transformation and a narrowing transformation. Everything is really simple. If the data type becomes larger (for example, it was int, but it became long), then the type becomes wider (out of 32 bits it becomes 64). And in this case, we do not risk losing data, because. if it fits into int, then it will fit into long even more, so we don’t notice this cast, since it is carried out automatically. narrowing . So to speak, so that we ourselves say: “Yes, I give myself an account of this. If something happens, it's your own fault."
public static void main(String []args){
   int intValue = 128;
   byte value = (byte)intValue;
   System.out.println(value);
}
So that later in this case they don’t say that “Your Java is bad” when they suddenly get -128 instead of 128) We remember that in byte 127 the upper value and everything above it, respectively, can be lost. When we explicitly converted our int to a byte, an overflow occurred and the value became -128.

Area of ​​visibility

This is the place in the code where this variable will perform its functions and store some value. When this area ends, the variable will cease to exist and will be erased from memory and. as you can already guess, it will be impossible to see or get its value! So what exactly is scope? Primitive Types in Java: Not So Primitive - 6The area is defined by a "block" - in general, any area closed in curly braces, the exit from which promises the deletion of the data declared in it. Or at least - hiding them from other blocks open outside the current one. In Java, scope is defined in two main ways:
  • class.
  • Method.
As I said, a variable is not visible to the code if it is defined outside of the block in which it was initialized. Let's see an example:
int x;
x = 6;
if (x >= 4) {
   int y = 3;
}
x = y;// переменная y здесь не видна!
And as a result we get an error:

Error:(10, 21) java: cannot find symbol
  symbol:   variable y
  location: class com.codeGym.test.type.Main
Scopes can be nested (if we declared a variable in the first, outer block, then it will be visible in the inner one).

Conclusion

Today we got acquainted with eight primitive types in Java. These types can be divided into four groups:
  • Integers: byte, short, int, long are signed integers.
  • Floating point numbers - this group includes float and double - types that store numbers up to a certain decimal place.
  • Boolean values ​​- boolean - store values ​​of type "true/false".
  • Symbols - this group includes the char type.
As the text above showed, the primitives in Java are not so primitive and allow you to solve many problems efficiently. But this introduces some peculiarities that should be kept in mind if we do not want to encounter unpredictable behavior of our program. As they say, you have to pay for everything. If we want a primitive with a “cool” (wide) range — something like a long — we sacrifice allocating a larger chunk of memory and vice versa. By saving memory and using byte, we get a limited range from -128 to 127.
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION